From f2435ad49f376897466f8d727b6ad8be4fda8380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Membr=C3=A9?= Date: Tue, 16 Jul 2024 15:26:27 +0200 Subject: [PATCH 01/65] Update main-build.conf --- main-build.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main-build.conf b/main-build.conf index 59bc7701b..78f4b4db3 100644 --- a/main-build.conf +++ b/main-build.conf @@ -14,6 +14,6 @@ # Version of Rudder used to build the plugin. # It defined the API/ABI used and it is important for binary compatibility branch-type=next -rudder-version=8.2.0~alpha1 +rudder-version=8.3.0~alpha1 common-version=2.1.1 private-version=2.1.0 From c4418d770790cffbe67bcd3ede096e644dfe988b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Membr=C3=A9?= Date: Tue, 16 Jul 2024 15:27:03 +0200 Subject: [PATCH 02/65] Update Jenkinsfile version --- Jenkinsfile | 2 +- Jenkinsfile-security | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 764dbc3e1..68ddb0712 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,6 +1,6 @@ def failedBuild = false -def minor_version = "8.2" +def minor_version = "8.3" def version = "${minor_version}" def changeUrl = env.CHANGE_URL def slackResponse = null diff --git a/Jenkinsfile-security b/Jenkinsfile-security index b238d1d8c..90ff09b2d 100644 --- a/Jenkinsfile-security +++ b/Jenkinsfile-security @@ -1,5 +1,5 @@ -def version = "8.2" +def version = "8.3" def changeUrl = env.CHANGE_URL def job = "" def errors = [] From 0626ec6b7eb43ac1f2b809de7d5596b1cda493ef Mon Sep 17 00:00:00 2001 From: "Francois @fanf42 Armand" Date: Sun, 3 Nov 2024 16:29:31 +0100 Subject: [PATCH 03/65] Fixes #25783: Remove NodeInfoService impact - public plugins --- .../apiauthorizations/EnablePluginImpl.scala | 4 +- .../apiauthorizations/EnablePluginImpl.scala | 6 +- .../rudder/plugin/ApiAuthorizationsConf.scala | 2 +- .../authbackends/EnablePluginImpl.scala | 4 +- .../authbackends/EnablePluginImpl.scala | 6 +- .../rudder/plugin/AuthBackendsConf.scala | 2 +- .../plugins/branding/EnablePluginImpl.scala | 4 +- .../plugins/branding/EnablePluginImpl.scala | 6 +- .../rudder/plugin/BrandingPluginConf.scala | 2 +- .../changevalidation/EnablePluginImpl.scala | 4 +- .../changevalidation/EnablePluginImpl.scala | 6 +- .../rudder/plugin/ChangeValidationConf.scala | 2 +- .../datasources/EnablePluginImpl.scala | 4 +- .../datasources/EnablePluginImpl.scala | 6 +- .../rudder/plugin/DataSourcesConf.scala | 6 +- .../datasources/api/DataSourceApiImpl.scala | 72 +++++++++---------- .../datasources/UpdateHttpDatasetTest.scala | 11 +-- .../api/RestDataSourceFilesTest.scala | 4 +- .../datasources/api/RestDataSourceTest.scala | 4 +- .../EnablePluginImpl.scala | 4 +- .../EnablePluginImpl.scala | 6 +- .../plugin/NodeExternalReportsConf.scala | 4 +- .../CreateNodeDetailsExtension.scala | 5 +- .../service/ReadExternalReports.scala | 11 +-- .../openscappolicies/EnablePluginImpl.scala | 4 +- .../openscappolicies/EnablePluginImpl.scala | 6 +- .../rudder/plugin/OpenscapPoliciesConf.scala | 4 +- .../openscappolicies/api/OpenScapApi.scala | 6 +- .../OpenScapNodeDetailsExtension.scala | 5 +- .../services/OpenScapReportReader.scala | 11 +-- .../api/OpenScapApiTest.scala | 2 +- .../scaleoutrelay/EnablePluginImpl.scala | 4 +- .../scaleoutrelay/EnablePluginImpl.scala | 6 +- .../rudder/plugin/ScaleOutRelayConf.scala | 2 +- 34 files changed, 112 insertions(+), 123 deletions(-) diff --git a/api-authorizations/src/main/scala-templates/default/com/normation/plugins/apiauthorizations/EnablePluginImpl.scala b/api-authorizations/src/main/scala-templates/default/com/normation/plugins/apiauthorizations/EnablePluginImpl.scala index 9cbd1b519..a94f25707 100644 --- a/api-authorizations/src/main/scala-templates/default/com/normation/plugins/apiauthorizations/EnablePluginImpl.scala +++ b/api-authorizations/src/main/scala-templates/default/com/normation/plugins/apiauthorizations/EnablePluginImpl.scala @@ -38,6 +38,6 @@ package com.normation.plugins.apiauthorizations import com.normation.plugins.PluginEnableImpl -import com.normation.rudder.services.nodes.NodeInfoService +import com.normation.rudder.facts.nodes.NodeFactRepository -final class CheckRudderPluginEnableImpl(nodeInfoService: NodeInfoService) extends PluginEnableImpl +final class CheckRudderPluginEnableImpl(nodeFactRepo: NodeFactRepository) extends PluginEnableImpl diff --git a/api-authorizations/src/main/scala-templates/limited/com/normation/plugins/apiauthorizations/EnablePluginImpl.scala b/api-authorizations/src/main/scala-templates/limited/com/normation/plugins/apiauthorizations/EnablePluginImpl.scala index 612ecdace..af29240d4 100644 --- a/api-authorizations/src/main/scala-templates/limited/com/normation/plugins/apiauthorizations/EnablePluginImpl.scala +++ b/api-authorizations/src/main/scala-templates/limited/com/normation/plugins/apiauthorizations/EnablePluginImpl.scala @@ -38,15 +38,15 @@ package com.normation.plugins.apiauthorizations import com.normation.plugins.LicensedPluginCheck -import com.normation.rudder.services.nodes.NodeInfoService +import com.normation.rudder.facts.nodes.NodeFactRepository import com.normation.zio.* -final class CheckRudderPluginEnableImpl(nodeInfoService: NodeInfoService) extends LicensedPluginCheck { +final class CheckRudderPluginEnableImpl(nodeFactRepo: NodeFactRepository) extends LicensedPluginCheck { // here are processed variables def pluginResourcePublickey = "${plugin-resource-publickey}" def pluginResourceLicense = "${plugin-resource-license}" def pluginDeclaredVersion = "${plugin-declared-version}" def pluginId = "${plugin-fullname}" - override def getNumberOfNodes: Int = nodeInfoService.getNumberOfManagedNodes.runNow + override def getNumberOfNodes: Int = nodeFactRepo.getNumberOfManagedNodes().runNow } diff --git a/api-authorizations/src/main/scala/bootstrap/rudder/plugin/ApiAuthorizationsConf.scala b/api-authorizations/src/main/scala/bootstrap/rudder/plugin/ApiAuthorizationsConf.scala index 55479ff81..d7bc07b62 100644 --- a/api-authorizations/src/main/scala/bootstrap/rudder/plugin/ApiAuthorizationsConf.scala +++ b/api-authorizations/src/main/scala/bootstrap/rudder/plugin/ApiAuthorizationsConf.scala @@ -55,7 +55,7 @@ class AclLevel(status: PluginStatus) extends ApiAuthorizationLevelService { */ object ApiAuthorizationsConf extends RudderPluginModule { // by build convention, we have only one of that on the classpath - lazy val pluginStatusService = new CheckRudderPluginEnableImpl(RudderConfig.nodeInfoService) + lazy val pluginStatusService = new CheckRudderPluginEnableImpl(RudderConfig.nodeFactRepository) // override default service level RudderConfig.apiAuthorizationLevelService.overrideLevel(new AclLevel(pluginStatusService)) diff --git a/auth-backends/src/main/scala-templates/default/com/normation/plugins/authbackends/EnablePluginImpl.scala b/auth-backends/src/main/scala-templates/default/com/normation/plugins/authbackends/EnablePluginImpl.scala index 0a6d4236e..b981d3395 100644 --- a/auth-backends/src/main/scala-templates/default/com/normation/plugins/authbackends/EnablePluginImpl.scala +++ b/auth-backends/src/main/scala-templates/default/com/normation/plugins/authbackends/EnablePluginImpl.scala @@ -38,10 +38,10 @@ package com.normation.plugins.authbackends import com.normation.plugins.PluginEnableImpl -import com.normation.rudder.services.nodes.NodeInfoService +import com.normation.rudder.facts.nodes.NodeFactRepository /* * The class will be loaded by ServiceLoader, it needs an empty constructor. */ -final class CheckRudderPluginEnableImpl(nodeInfoService: NodeInfoService) extends PluginEnableImpl +final class CheckRudderPluginEnableImpl(nodeFactRepo: NodeFactRepository) extends PluginEnableImpl diff --git a/auth-backends/src/main/scala-templates/limited/com/normation/plugins/authbackends/EnablePluginImpl.scala b/auth-backends/src/main/scala-templates/limited/com/normation/plugins/authbackends/EnablePluginImpl.scala index e58dcbbc9..0eead45b1 100644 --- a/auth-backends/src/main/scala-templates/limited/com/normation/plugins/authbackends/EnablePluginImpl.scala +++ b/auth-backends/src/main/scala-templates/limited/com/normation/plugins/authbackends/EnablePluginImpl.scala @@ -38,7 +38,7 @@ package com.normation.plugins.authbackends import com.normation.plugins.LicensedPluginCheck -import com.normation.rudder.services.nodes.NodeInfoService +import com.normation.rudder.facts.nodes.NodeFactRepository import com.normation.zio.* /* @@ -48,12 +48,12 @@ import com.normation.zio.* * * The class will be loaded by ServiceLoader, it needs an empty constructor. */ -final class CheckRudderPluginEnableImpl(nodeInfoService: NodeInfoService) extends LicensedPluginCheck { +final class CheckRudderPluginEnableImpl(nodeFactRepo: NodeFactRepository) extends LicensedPluginCheck { // here are processed variables def pluginResourcePublickey = "${plugin-resource-publickey}" def pluginResourceLicense = "${plugin-resource-license}" def pluginDeclaredVersion = "${plugin-declared-version}" def pluginId = "${plugin-fullname}" - override def getNumberOfNodes: Int = nodeInfoService.getNumberOfManagedNodes.runNow + override def getNumberOfNodes: Int = nodeFactRepo.getNumberOfManagedNodes.runNow } diff --git a/auth-backends/src/main/scala/bootstrap/rudder/plugin/AuthBackendsConf.scala b/auth-backends/src/main/scala/bootstrap/rudder/plugin/AuthBackendsConf.scala index 18b540289..3c3b03c72 100644 --- a/auth-backends/src/main/scala/bootstrap/rudder/plugin/AuthBackendsConf.scala +++ b/auth-backends/src/main/scala/bootstrap/rudder/plugin/AuthBackendsConf.scala @@ -133,7 +133,7 @@ object AuthBackendsConf extends RudderPluginModule { val DISPLAY_LOGIN_FORM_PROP = "rudder.auth.displayLoginForm" // by build convention, we have only one of that on the classpath - lazy val pluginStatusService = new CheckRudderPluginEnableImpl(RudderConfig.nodeInfoService) + lazy val pluginStatusService = new CheckRudderPluginEnableImpl(RudderConfig.nodeFactRepository) lazy val authBackendsProvider = new AuthBackendsProvider { def authenticationBackends = Set("ldap") diff --git a/branding/src/main/scala-templates/default/com/normation/plugins/branding/EnablePluginImpl.scala b/branding/src/main/scala-templates/default/com/normation/plugins/branding/EnablePluginImpl.scala index c94bbb418..55f39b01d 100644 --- a/branding/src/main/scala-templates/default/com/normation/plugins/branding/EnablePluginImpl.scala +++ b/branding/src/main/scala-templates/default/com/normation/plugins/branding/EnablePluginImpl.scala @@ -38,6 +38,6 @@ package com.normation.plugins.branding import com.normation.plugins.PluginEnableImpl -import com.normation.rudder.services.nodes.NodeInfoService +import com.normation.rudder.facts.nodes.NodeFactRepository -final class CheckRudderPluginEnableImpl(nodeInfoService: NodeInfoService) extends PluginEnableImpl +final class CheckRudderPluginEnableImpl(nodeFactRepo: NodeFactRepository) extends PluginEnableImpl diff --git a/branding/src/main/scala-templates/limited/com/normation/plugins/branding/EnablePluginImpl.scala b/branding/src/main/scala-templates/limited/com/normation/plugins/branding/EnablePluginImpl.scala index 8236a551b..033928733 100644 --- a/branding/src/main/scala-templates/limited/com/normation/plugins/branding/EnablePluginImpl.scala +++ b/branding/src/main/scala-templates/limited/com/normation/plugins/branding/EnablePluginImpl.scala @@ -38,15 +38,15 @@ package com.normation.plugins.branding import com.normation.plugins.LicensedPluginCheck -import com.normation.rudder.services.nodes.NodeInfoService +import com.normation.rudder.facts.nodes.NodeFactRepository import com.normation.zio.* -final class CheckRudderPluginEnableImpl(nodeInfoService: NodeInfoService) extends LicensedPluginCheck { +final class CheckRudderPluginEnableImpl(nodeFactRepo: NodeFactRepository) extends LicensedPluginCheck { // here are processed variables def pluginResourcePublickey = "${plugin-resource-publickey}" def pluginResourceLicense = "${plugin-resource-license}" def pluginDeclaredVersion = "${plugin-declared-version}" def pluginId = "${plugin-fullname}" - override def getNumberOfNodes: Int = nodeInfoService.getNumberOfManagedNodes.runNow + override def getNumberOfNodes: Int = nodeFactRepo.getNumberOfManagedNodes().runNow } diff --git a/branding/src/main/scala/bootstrap/rudder/plugin/BrandingPluginConf.scala b/branding/src/main/scala/bootstrap/rudder/plugin/BrandingPluginConf.scala index 28f6bf758..7d493cb7b 100644 --- a/branding/src/main/scala/bootstrap/rudder/plugin/BrandingPluginConf.scala +++ b/branding/src/main/scala/bootstrap/rudder/plugin/BrandingPluginConf.scala @@ -53,7 +53,7 @@ import com.normation.plugins.branding.snippet.LoginBranding object BrandingPluginConf extends RudderPluginModule { // by build convention, we have only one of that on the classpath - lazy val pluginStatusService = new CheckRudderPluginEnableImpl(RudderConfig.nodeInfoService) + lazy val pluginStatusService = new CheckRudderPluginEnableImpl(RudderConfig.nodeFactRepository) lazy val pluginDef: BrandingPluginDef = new BrandingPluginDef(BrandingPluginConf.pluginStatusService) val brandingConfService: BrandingConfService = new BrandingConfService(BrandingConfService.defaultConfigFilePath) diff --git a/change-validation/src/main/scala-templates/default/com/normation/plugins/changevalidation/EnablePluginImpl.scala b/change-validation/src/main/scala-templates/default/com/normation/plugins/changevalidation/EnablePluginImpl.scala index 047ed8707..a97af0bab 100644 --- a/change-validation/src/main/scala-templates/default/com/normation/plugins/changevalidation/EnablePluginImpl.scala +++ b/change-validation/src/main/scala-templates/default/com/normation/plugins/changevalidation/EnablePluginImpl.scala @@ -38,10 +38,10 @@ package com.normation.plugins.changevalidation import com.normation.plugins.PluginEnableImpl -import com.normation.rudder.services.nodes.NodeInfoService +import com.normation.rudder.facts.nodes.NodeFactRepository /* * The class will be loaded by ServiceLoader, it needs an empty constructor. */ -final class CheckRudderPluginEnableImpl(nodeInfoService: NodeInfoService) extends PluginEnableImpl +final class CheckRudderPluginEnableImpl(nodeFactRepo: NodeFactRepository) extends PluginEnableImpl diff --git a/change-validation/src/main/scala-templates/limited/com/normation/plugins/changevalidation/EnablePluginImpl.scala b/change-validation/src/main/scala-templates/limited/com/normation/plugins/changevalidation/EnablePluginImpl.scala index a250f3692..be78c4fd5 100644 --- a/change-validation/src/main/scala-templates/limited/com/normation/plugins/changevalidation/EnablePluginImpl.scala +++ b/change-validation/src/main/scala-templates/limited/com/normation/plugins/changevalidation/EnablePluginImpl.scala @@ -38,7 +38,7 @@ package com.normation.plugins.changevalidation import com.normation.plugins.LicensedPluginCheck -import com.normation.rudder.services.nodes.NodeInfoService +import com.normation.rudder.facts.nodes.NodeFactRepository import com.normation.zio.* /* @@ -48,12 +48,12 @@ import com.normation.zio.* * * The class will be loaded by ServiceLoader, it needs an empty constructor. */ -final class CheckRudderPluginEnableImpl(nodeInfoService: NodeInfoService) extends LicensedPluginCheck { +final class CheckRudderPluginEnableImpl(nodeFactRepo: NodeFactRepository) extends LicensedPluginCheck { // here are processed variables def pluginResourcePublickey = "${plugin-resource-publickey}" def pluginResourceLicense = "${plugin-resource-license}" def pluginDeclaredVersion = "${plugin-declared-version}" def pluginId = "${plugin-fullname}" - override def getNumberOfNodes: Int = nodeInfoService.getNumberOfManagedNodes.runNow + override def getNumberOfNodes: Int = nodeFactRepo.getNumberOfManagedNodes().runNow } 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 ffe4b00f9..ba507be63 100644 --- a/change-validation/src/main/scala/bootstrap/rudder/plugin/ChangeValidationConf.scala +++ b/change-validation/src/main/scala/bootstrap/rudder/plugin/ChangeValidationConf.scala @@ -226,7 +226,7 @@ object ChangeValidationConf extends RudderPluginModule { "/opt/rudder/etc/plugins/change-validation.conf" ) // by build convention, we have only one of that on the classpath - lazy val pluginStatusService = new CheckRudderPluginEnableImpl(RudderConfig.nodeInfoService) + lazy val pluginStatusService = new CheckRudderPluginEnableImpl(RudderConfig.nodeFactRepository) lazy val roWorkflowRepository = new RoWorkflowJdbcRepository(RudderConfig.doobie) lazy val woWorkflowRepository = new WoWorkflowJdbcRepository(RudderConfig.doobie) diff --git a/datasources/src/main/scala-templates/default/com/normation/plugins/datasources/EnablePluginImpl.scala b/datasources/src/main/scala-templates/default/com/normation/plugins/datasources/EnablePluginImpl.scala index 88eeaaf7c..838c85d8b 100644 --- a/datasources/src/main/scala-templates/default/com/normation/plugins/datasources/EnablePluginImpl.scala +++ b/datasources/src/main/scala-templates/default/com/normation/plugins/datasources/EnablePluginImpl.scala @@ -38,6 +38,6 @@ package com.normation.plugins.datasources import com.normation.plugins.PluginEnableImpl -import com.normation.rudder.services.nodes.NodeInfoService +import com.normation.rudder.facts.nodes.NodeFactRepository -final class CheckRudderPluginEnableImpl(nodeInfoService: NodeInfoService) extends PluginEnableImpl +final class CheckRudderPluginEnableImpl(nodeFactRepo: NodeFactRepository) extends PluginEnableImpl diff --git a/datasources/src/main/scala-templates/limited/com/normation/plugins/datasources/EnablePluginImpl.scala b/datasources/src/main/scala-templates/limited/com/normation/plugins/datasources/EnablePluginImpl.scala index b660dab21..4bb3db108 100644 --- a/datasources/src/main/scala-templates/limited/com/normation/plugins/datasources/EnablePluginImpl.scala +++ b/datasources/src/main/scala-templates/limited/com/normation/plugins/datasources/EnablePluginImpl.scala @@ -38,15 +38,15 @@ package com.normation.plugins.datasources import com.normation.plugins.LicensedPluginCheck -import com.normation.rudder.services.nodes.NodeInfoService +import com.normation.rudder.facts.nodes.NodeFactRepository import com.normation.zio.* -final class CheckRudderPluginEnableImpl(nodeInfoService: NodeInfoService) extends LicensedPluginCheck { +final class CheckRudderPluginEnableImpl(nodeFactRepo: NodeFactRepository) extends LicensedPluginCheck { // here are processed variables def pluginResourcePublickey = "${plugin-resource-publickey}" def pluginResourceLicense = "${plugin-resource-license}" def pluginDeclaredVersion = "${plugin-declared-version}" def pluginId = "${plugin-fullname}" - override def getNumberOfNodes: Int = nodeInfoService.getNumberOfManagedNodes.runNow + override def getNumberOfNodes: Int = nodeFactRepo.getNumberOfManagedNodes().runNow } diff --git a/datasources/src/main/scala/bootstrap/rudder/plugin/DataSourcesConf.scala b/datasources/src/main/scala/bootstrap/rudder/plugin/DataSourcesConf.scala index 43da51f8b..fee61b9f2 100644 --- a/datasources/src/main/scala/bootstrap/rudder/plugin/DataSourcesConf.scala +++ b/datasources/src/main/scala/bootstrap/rudder/plugin/DataSourcesConf.scala @@ -74,7 +74,7 @@ object DatasourcesConf extends RudderPluginModule { import bootstrap.liftweb.RudderConfig as Cfg // by build convention, we have only one of that on the classpath - lazy val pluginStatusService = new CheckRudderPluginEnableImpl(RudderConfig.nodeInfoService) + lazy val pluginStatusService = new CheckRudderPluginEnableImpl(RudderConfig.nodeFactRepository) lazy val regenerationHook = new OnUpdatedNodeRegenerate(RudderConfig.asyncDeploymentAgent) @@ -112,10 +112,8 @@ object DatasourcesConf extends RudderPluginModule { ) val dataSourceApi9 = new DataSourceApiImpl( - Cfg.restExtractorService, dataSourceRepository, - Cfg.nodeInfoService, - Cfg.woNodeRepository, + Cfg.nodeFactRepository, Cfg.stringUuidGenerator ) diff --git a/datasources/src/main/scala/com/normation/plugins/datasources/api/DataSourceApiImpl.scala b/datasources/src/main/scala/com/normation/plugins/datasources/api/DataSourceApiImpl.scala index 0815dfef3..aba9c4777 100644 --- a/datasources/src/main/scala/com/normation/plugins/datasources/api/DataSourceApiImpl.scala +++ b/datasources/src/main/scala/com/normation/plugins/datasources/api/DataSourceApiImpl.scala @@ -51,29 +51,30 @@ import com.normation.plugins.datasources.RestResponseMessage import com.normation.plugins.datasources.UpdateCause import com.normation.plugins.datasources.api.DataSourceApi as API import com.normation.rudder.api.ApiVersion -import com.normation.rudder.domain.nodes.Node import com.normation.rudder.domain.properties.CompareProperties import com.normation.rudder.domain.properties.GenericProperty.* -import com.normation.rudder.repository.WoNodeRepository +import com.normation.rudder.facts.nodes.ChangeContext +import com.normation.rudder.facts.nodes.CoreNodeFact +import com.normation.rudder.facts.nodes.NodeFactRepository +import com.normation.rudder.facts.nodes.NodeSecurityContext import com.normation.rudder.rest.* import com.normation.rudder.rest.implicits.* import com.normation.rudder.rest.lift.DefaultParams import com.normation.rudder.rest.lift.LiftApiModule import com.normation.rudder.rest.lift.LiftApiModule0 import com.normation.rudder.rest.lift.LiftApiModuleProvider -import com.normation.rudder.services.nodes.NodeInfoService import com.normation.utils.StringUuidGenerator import io.scalaland.chimney.syntax.* import net.liftweb.http.LiftResponse import net.liftweb.http.Req +import org.joda.time.DateTime +import zio.Chunk import zio.syntax.* class DataSourceApiImpl( - extractor: RestExtractorService, - dataSourceRepo: DataSourceRepository with DataSourceUpdateCallbacks, - nodeInfoService: NodeInfoService, - nodeRepos: WoNodeRepository, - uuidGen: StringUuidGenerator + dataSourceRepo: DataSourceRepository with DataSourceUpdateCallbacks, + nodeFactRepo: NodeFactRepository, + uuidGen: StringUuidGenerator ) extends LiftApiModuleProvider[API] { api => @@ -81,8 +82,6 @@ class DataSourceApiImpl( override def schemas: ApiModuleProvider[API] = API - type ActionType = RestUtils.ActionType - import com.normation.plugins.datasources.DataSourceExtractor.OptionalJson.* def getLiftEndpoints(): List[LiftApiModule] = { @@ -106,7 +105,6 @@ class DataSourceApiImpl( object ReloadAllDatasourcesOneNode extends LiftApiModule { val schema: DataSourceApi.ReloadAllDatasourcesOneNode.type = API.ReloadAllDatasourcesOneNode - val restExtractor = extractor def process( version: ApiVersion, path: ApiPath, @@ -126,7 +124,6 @@ class DataSourceApiImpl( object ReloadOneDatasourceAllNodes extends LiftApiModule { val schema: DataSourceApi.ReloadOneDatasourceAllNodes.type = API.ReloadOneDatasourceAllNodes - val restExtractor = extractor def process( version: ApiVersion, path: ApiPath, @@ -146,7 +143,6 @@ class DataSourceApiImpl( object ReloadOneDatasourceOneNode extends LiftApiModule { val schema: DataSourceApi.ReloadOneDatasourceOneNode.type = API.ReloadOneDatasourceOneNode - val restExtractor = extractor def process( version: ApiVersion, path: ApiPath, @@ -167,7 +163,6 @@ class DataSourceApiImpl( object ClearValueOneDatasourceAllNodes extends LiftApiModule { val schema: DataSourceApi.ClearValueOneDatasourceAllNodes.type = API.ClearValueOneDatasourceAllNodes - val restExtractor = extractor def process( version: ApiVersion, path: ApiPath, @@ -182,8 +177,8 @@ class DataSourceApiImpl( UpdateCause(modId, authzToken.qc.actor, Some(s"API request to clear '${datasourceId}' on node '${nodeId.value}'"), false) (for { - nodes <- nodeInfoService.getAllNodes() - _ <- nodes.values.accumulate(node => erase(cause(node.id), node, DataSourceId(datasourceId))) + nodes <- nodeFactRepo.getAll()(authzToken.qc) + _ <- nodes.values.accumulate(node => erase(cause(node.id), node, DataSourceId(datasourceId), authzToken.qc.nodePerms)) res = s"Data for all nodes, for data source '${datasourceId}', cleared" } yield res) .chainError(s"Could not clear data source property '${datasourceId}'") @@ -193,7 +188,6 @@ class DataSourceApiImpl( object ClearValueOneDatasourceOneNode extends LiftApiModule { val schema: DataSourceApi.ClearValueOneDatasourceOneNode.type = API.ClearValueOneDatasourceOneNode - val restExtractor = extractor def process( version: ApiVersion, path: ApiPath, @@ -207,13 +201,12 @@ class DataSourceApiImpl( ModificationId(uuidGen.newUuid), authzToken.qc.actor, Some(s"API request to clear '${datasourceId}' on node '${nodeId}'"), - false + triggeredByGeneration = false ) (for { - optNode <- nodeInfoService.getNodeInfo(NodeId(nodeId)) - node <- optNode.notOptional(s"Node with ID '${nodeId}' was not found") - updated <- erase(cause, node.node, DataSourceId(datasourceId)) + node <- nodeFactRepo.get(NodeId(nodeId))(authzToken.qc).notOptional(s"Node with ID '${nodeId}' was not found") + updated <- erase(cause, node, DataSourceId(datasourceId), authzToken.qc.nodePerms) res = s"Data for node '${nodeId}', for data source '${datasourceId}', cleared" } yield res) .chainError(s"Could not clear data source property '${datasourceId}'") @@ -222,9 +215,8 @@ class DataSourceApiImpl( } object ReloadAllDatasourcesAllNodes extends LiftApiModule0 { - val schema: DataSourceApi.ReloadAllDatasourcesAllNodes.type = API.ReloadAllDatasourcesAllNodes - val restExtractor = extractor - def process0(version: ApiVersion, path: ApiPath, req: Req, params: DefaultParams, authzToken: AuthzToken): LiftResponse = { + val schema: DataSourceApi.ReloadAllDatasourcesAllNodes.type = API.ReloadAllDatasourcesAllNodes + def process0(version: ApiVersion, path: ApiPath, req: Req, params: DefaultParams, authzToken: AuthzToken): LiftResponse = { // reloadData All Nodes All Datasources dataSourceRepo .onUserAskUpdateAllNodes(authzToken.qc.actor) @@ -235,9 +227,8 @@ class DataSourceApiImpl( } object GetAllDataSources extends LiftApiModule0 { - val schema: DataSourceApi.GetAllDataSources.type = API.GetAllDataSources - val restExtractor = extractor - def process0(version: ApiVersion, path: ApiPath, req: Req, params: DefaultParams, authzToken: AuthzToken): LiftResponse = { + val schema: DataSourceApi.GetAllDataSources.type = API.GetAllDataSources + def process0(version: ApiVersion, path: ApiPath, req: Req, params: DefaultParams, authzToken: AuthzToken): LiftResponse = { (for { sources <- dataSourceRepo.getAll } yield { @@ -250,7 +241,6 @@ class DataSourceApiImpl( object GetDataSource extends LiftApiModule { val schema: DataSourceApi.GetDataSource.type = API.GetDataSource - val restExtractor = extractor def process( version: ApiVersion, path: ApiPath, @@ -270,7 +260,6 @@ class DataSourceApiImpl( object DeleteDataSource extends LiftApiModule { val schema: DataSourceApi.DeleteDataSource.type = API.DeleteDataSource - val restExtractor = extractor def process( version: ApiVersion, path: ApiPath, @@ -296,9 +285,8 @@ class DataSourceApiImpl( } object CreateDataSource extends LiftApiModule0 { - val schema: DataSourceApi.CreateDataSource.type = API.CreateDataSource - val restExtractor = extractor - def process0(version: ApiVersion, path: ApiPath, req: Req, params: DefaultParams, authzToken: AuthzToken): LiftResponse = { + val schema: DataSourceApi.CreateDataSource.type = API.CreateDataSource + def process0(version: ApiVersion, path: ApiPath, req: Req, params: DefaultParams, authzToken: AuthzToken): LiftResponse = { (for { source <- extractNewDataSource(req).toIO _ <- dataSourceRepo.save(source) @@ -311,7 +299,6 @@ class DataSourceApiImpl( object UpdateDataSource extends LiftApiModule { val schema: DataSourceApi.UpdateDataSource.type = API.UpdateDataSource - val restExtractor = extractor def process( version: ApiVersion, path: ApiPath, @@ -336,20 +323,25 @@ class DataSourceApiImpl( } /// utilities /// - private[this] def erase(cause: UpdateCause, node: Node, datasourceId: DataSourceId): IOResult[NodeUpdateResult] = { + private[this] def erase( + cause: UpdateCause, + node: CoreNodeFact, + datasourceId: DataSourceId, + nodePerms: NodeSecurityContext + ): IOResult[NodeUpdateResult] = { val newProp = DataSource.nodeProperty(datasourceId.value, "".toConfigValue) node.properties.find(_.name == newProp.name) match { case None => NodeUpdateResult.Unchanged(node.id).succeed case Some(p) => if (p.provider == newProp.provider) { for { - newProps <- CompareProperties.updateProperties(node.properties, Some(newProp :: Nil)).toIO - newNode = node.copy(properties = newProps) - nodeUpdated <- nodeRepos - .updateNode(newNode, cause.modId, cause.actor, cause.reason) - .chainError(s"Cannot clear value for node '${node.id.value}' for property '${newProp.name}'") + newProps <- CompareProperties.updateProperties(node.properties.toList, Some(newProp :: Nil)).toIO + newNode = node.copy(properties = Chunk.fromIterable(newProps)) + _ <- nodeFactRepo + .save(newNode)(ChangeContext(cause.modId, cause.actor, DateTime.now, cause.reason, None, nodePerms)) + .chainError(s"Cannot clear value for node '${node.id.value}' for property '${newProp.name}'") } yield { - NodeUpdateResult.Updated(nodeUpdated.id) + NodeUpdateResult.Updated(newNode.id) } } else { Unexpected( diff --git a/datasources/src/test/scala/com/normation/plugins/datasources/UpdateHttpDatasetTest.scala b/datasources/src/test/scala/com/normation/plugins/datasources/UpdateHttpDatasetTest.scala index 5db927279..48764dfd2 100644 --- a/datasources/src/test/scala/com/normation/plugins/datasources/UpdateHttpDatasetTest.scala +++ b/datasources/src/test/scala/com/normation/plugins/datasources/UpdateHttpDatasetTest.scala @@ -60,7 +60,6 @@ import com.normation.rudder.repository.RoParameterRepository import com.normation.rudder.services.nodes.PropertyEngineServiceImpl import com.normation.rudder.services.policies.InterpolatedValueCompilerImpl import com.normation.rudder.services.policies.NodeConfigData -import com.normation.rudder.tenants.DefaultTenantService import com.normation.utils.StringUuidGeneratorImpl import com.normation.zio.* import com.softwaremill.quicklens.* @@ -503,15 +502,9 @@ class UpdateHttpDatasetTest extends Specification with BoxSpecMatcher with Logga } } } - ts <- DefaultTenantService.make(Nil) - repo <- CoreNodeFactRepository.make( - NoopFactStorage, - NoopGetNodesBySoftwareName, - ts, - Map(), + repo <- CoreNodeFactRepository.makeNoop( initNodeInfo, - Chunk(updateCallback), - Chunk() + callbacks = Chunk(updateCallback) ) } yield (updates, repo)).runNow } diff --git a/datasources/src/test/scala/com/normation/plugins/datasources/api/RestDataSourceFilesTest.scala b/datasources/src/test/scala/com/normation/plugins/datasources/api/RestDataSourceFilesTest.scala index 566903e33..e2713e3a8 100644 --- a/datasources/src/test/scala/com/normation/plugins/datasources/api/RestDataSourceFilesTest.scala +++ b/datasources/src/test/scala/com/normation/plugins/datasources/api/RestDataSourceFilesTest.scala @@ -61,10 +61,8 @@ class RestDataSourceFilesTest extends ZIOSpecDefault { val mockNodes = new MockNodes() val dataSourceApi9 = new DataSourceApiImpl( - restTestSetUp.restExtractorService, datasourceRepo, - mockNodes.nodeInfoService, - null, + mockNodes.nodeFactRepo, restTestSetUp.uuidGen ) diff --git a/datasources/src/test/scala/com/normation/plugins/datasources/api/RestDataSourceTest.scala b/datasources/src/test/scala/com/normation/plugins/datasources/api/RestDataSourceTest.scala index bcb3ecdfe..9a11ac0f8 100644 --- a/datasources/src/test/scala/com/normation/plugins/datasources/api/RestDataSourceTest.scala +++ b/datasources/src/test/scala/com/normation/plugins/datasources/api/RestDataSourceTest.scala @@ -65,10 +65,8 @@ class RestDataSourceTest extends Specification { val mockNodes = new MockNodes() val dataSourceApi9 = new DataSourceApiImpl( - restTestSetUp.restExtractorService, datasourceRepo, - mockNodes.nodeInfoService, - null, + mockNodes.nodeFactRepo, restTestSetUp.uuidGen ) diff --git a/node-external-reports/src/main/scala-templates/default/com/normation/plugins/nodeexternalreports/EnablePluginImpl.scala b/node-external-reports/src/main/scala-templates/default/com/normation/plugins/nodeexternalreports/EnablePluginImpl.scala index 1c7e55f34..19e0326c8 100644 --- a/node-external-reports/src/main/scala-templates/default/com/normation/plugins/nodeexternalreports/EnablePluginImpl.scala +++ b/node-external-reports/src/main/scala-templates/default/com/normation/plugins/nodeexternalreports/EnablePluginImpl.scala @@ -38,10 +38,10 @@ package com.normation.plugins.nodeexternalreports import com.normation.plugins.PluginEnableImpl -import com.normation.rudder.services.nodes.NodeInfoService +import com.normation.rudder.facts.nodes.NodeFactRepository /* * The class will be loaded by ServiceLoader, it needs an empty constructor. */ -final class CheckRudderPluginEnableImpl(nodeInfoService: NodeInfoService) extends PluginEnableImpl +final class CheckRudderPluginEnableImpl(nodeFactRepo: NodeFactRepository) extends PluginEnableImpl diff --git a/node-external-reports/src/main/scala-templates/limited/com/normation/plugins/nodeexternalreports/EnablePluginImpl.scala b/node-external-reports/src/main/scala-templates/limited/com/normation/plugins/nodeexternalreports/EnablePluginImpl.scala index 807d0586d..bdeb69421 100644 --- a/node-external-reports/src/main/scala-templates/limited/com/normation/plugins/nodeexternalreports/EnablePluginImpl.scala +++ b/node-external-reports/src/main/scala-templates/limited/com/normation/plugins/nodeexternalreports/EnablePluginImpl.scala @@ -38,7 +38,7 @@ package com.normation.plugins.nodeexternalreports import com.normation.plugins.LicensedPluginCheck -import com.normation.rudder.services.nodes.NodeInfoService +import com.normation.rudder.facts.nodes.NodeFactRepository import com.normation.zio.* /* @@ -49,12 +49,12 @@ import com.normation.zio.* * The class will be loaded by ServiceLoader, it needs an empty constructor. */ -final class CheckRudderPluginEnableImpl(nodeInfoService: NodeInfoService) extends LicensedPluginCheck { +final class CheckRudderPluginEnableImpl(nodeFactRepo: NodeFactRepository) extends LicensedPluginCheck { // here are processed variables def pluginResourcePublickey = "${plugin-resource-publickey}" def pluginResourceLicense = "${plugin-resource-license}" def pluginDeclaredVersion = "${plugin-declared-version}" def pluginId = "${plugin-fullname}" - override def getNumberOfNodes: Int = nodeInfoService.getNumberOfManagedNodes.runNow + override def getNumberOfNodes: Int = nodeFactRepo.getNumberOfManagedNodes().runNow } diff --git a/node-external-reports/src/main/scala/bootstrap/rudder/plugin/NodeExternalReportsConf.scala b/node-external-reports/src/main/scala/bootstrap/rudder/plugin/NodeExternalReportsConf.scala index d19b393bc..65e1e4672 100644 --- a/node-external-reports/src/main/scala/bootstrap/rudder/plugin/NodeExternalReportsConf.scala +++ b/node-external-reports/src/main/scala/bootstrap/rudder/plugin/NodeExternalReportsConf.scala @@ -61,11 +61,11 @@ object NodeExternalReportsConf extends RudderPluginModule { x } - new ReadExternalReports(RudderConfig.nodeInfoService, configPath) + new ReadExternalReports(RudderConfig.nodeFactRepository, configPath) } // by build convention, we have only one of that on the classpath - lazy val pluginStatusService = new CheckRudderPluginEnableImpl(RudderConfig.nodeInfoService) + lazy val pluginStatusService = new CheckRudderPluginEnableImpl(RudderConfig.nodeFactRepository) lazy val externalNodeReportApi = new NodeExternalReportApi(readReport) diff --git a/node-external-reports/src/main/scala/com/normation/plugins/nodeexternalreports/extension/CreateNodeDetailsExtension.scala b/node-external-reports/src/main/scala/com/normation/plugins/nodeexternalreports/extension/CreateNodeDetailsExtension.scala index 28bddf3b9..8cb14b000 100644 --- a/node-external-reports/src/main/scala/com/normation/plugins/nodeexternalreports/extension/CreateNodeDetailsExtension.scala +++ b/node-external-reports/src/main/scala/com/normation/plugins/nodeexternalreports/extension/CreateNodeDetailsExtension.scala @@ -40,10 +40,13 @@ import com.normation.plugins.PluginExtensionPoint import com.normation.plugins.PluginStatus import com.normation.plugins.nodeexternalreports.service.NodeExternalReport import com.normation.plugins.nodeexternalreports.service.ReadExternalReports +import com.normation.rudder.users.CurrentUser import com.normation.rudder.web.components.ShowNodeDetailsFromNode + import net.liftweb.common.* import net.liftweb.util.CssSel import net.liftweb.util.Helpers.* + import scala.reflect.ClassTag import scala.xml.NodeSeq @@ -63,7 +66,7 @@ class CreateNodeDetailsExtension(externalReport: ReadExternalReports, val status */ def addExternalReportTab(snippet: ShowNodeDetailsFromNode)(xml: NodeSeq) = { - val (tabTitle, content) = externalReport.getExternalReports(snippet.nodeId) match { + val (tabTitle, content) = externalReport.getExternalReports(snippet.nodeId)(CurrentUser.queryContext) match { case eb: EmptyBox => val e = eb ?~! "Can not display external reports for that node" ("External reports",
{e.messageChain}
) diff --git a/node-external-reports/src/main/scala/com/normation/plugins/nodeexternalreports/service/ReadExternalReports.scala b/node-external-reports/src/main/scala/com/normation/plugins/nodeexternalreports/service/ReadExternalReports.scala index 4f5047ad1..5a6bfa41c 100644 --- a/node-external-reports/src/main/scala/com/normation/plugins/nodeexternalreports/service/ReadExternalReports.scala +++ b/node-external-reports/src/main/scala/com/normation/plugins/nodeexternalreports/service/ReadExternalReports.scala @@ -38,7 +38,8 @@ package com.normation.plugins.nodeexternalreports.service import com.normation.box.* import com.normation.inventory.domain.NodeId -import com.normation.rudder.services.nodes.NodeInfoService +import com.normation.rudder.facts.nodes.NodeFactRepository +import com.normation.rudder.facts.nodes.QueryContext import com.typesafe.config.* import java.io.File import net.liftweb.common.* @@ -74,7 +75,7 @@ final case class NodeExternalReports( * Read the reports configuration file and build the * awaited reports */ -class ReadExternalReports(nodeInfoService: NodeInfoService, val reportConfigFile: String) extends Loggable { +class ReadExternalReports(nodeFactRepo: NodeFactRepository, val reportConfigFile: String) extends Loggable { private[this] var config: Box[ExternalReports] = null @@ -124,12 +125,12 @@ class ReadExternalReports(nodeInfoService: NodeInfoService, val reportConfigFile * with the correct report file for that node. * A Node says that no file was found */ - def getExternalReports(nodeId: NodeId): Box[NodeExternalReports] = { + def getExternalReports(nodeId: NodeId)(implicit qc: QueryContext): Box[NodeExternalReports] = { if (config == null) loadAndUpdateConfig() for { conf <- config - optNode <- nodeInfoService.getNodeInfo(nodeId).toBox + optNode <- nodeFactRepo.get(nodeId).toBox node <- optNode match { case None => Failure(s"The node with ID '${nodeId}' was not found, we can't add external information") case Some(n) => Full(n) @@ -141,7 +142,7 @@ class ReadExternalReports(nodeInfoService: NodeInfoService, val reportConfigFile case (key, report) => val fileName = { val uuidName = report.reportName(node.id.value).toLowerCase - val hostnameName = report.reportName(node.hostname).toLowerCase + val hostnameName = report.reportName(node.fqdn).toLowerCase if ((new File(report.rootDirectory, hostnameName)).exists) { Some(hostnameName) diff --git a/openscap/src/main/scala-templates/default/com/normation/plugins/openscappolicies/EnablePluginImpl.scala b/openscap/src/main/scala-templates/default/com/normation/plugins/openscappolicies/EnablePluginImpl.scala index bdcc602bd..b5e812a0d 100644 --- a/openscap/src/main/scala-templates/default/com/normation/plugins/openscappolicies/EnablePluginImpl.scala +++ b/openscap/src/main/scala-templates/default/com/normation/plugins/openscappolicies/EnablePluginImpl.scala @@ -38,10 +38,10 @@ package com.normation.plugins.openscappolicies import com.normation.plugins.PluginEnableImpl -import com.normation.rudder.services.nodes.NodeInfoService +import com.normation.rudder.facts.nodes.NodeFactRepository /* * The class will be loaded by ServiceLoader, it needs an empty constructor. */ -final class CheckRudderPluginEnableImpl(nodeInfoService: NodeInfoService) extends PluginEnableImpl +final class CheckRudderPluginEnableImpl(nodeFactRepo: NodeFactRepository) extends PluginEnableImpl diff --git a/openscap/src/main/scala-templates/limited/com/normation/plugins/openscappolicies/EnablePluginImpl.scala b/openscap/src/main/scala-templates/limited/com/normation/plugins/openscappolicies/EnablePluginImpl.scala index 64a8293e3..80282f87c 100644 --- a/openscap/src/main/scala-templates/limited/com/normation/plugins/openscappolicies/EnablePluginImpl.scala +++ b/openscap/src/main/scala-templates/limited/com/normation/plugins/openscappolicies/EnablePluginImpl.scala @@ -38,7 +38,7 @@ package com.normation.plugins.openscappolicies import com.normation.plugins.LicensedPluginCheck -import com.normation.rudder.services.nodes.NodeInfoService +import com.normation.rudder.facts.nodes.NodeFactRepository import com.normation.zio.* /* @@ -48,12 +48,12 @@ import com.normation.zio.* * * The class will be loaded by ServiceLoader, it needs an empty constructor. */ -final class CheckRudderPluginEnableImpl(nodeInfoService: NodeInfoService) extends LicensedPluginCheck { +final class CheckRudderPluginEnableImpl(nodeFactRepo: NodeFactRepository) extends LicensedPluginCheck { // here are processed variables def pluginResourcePublickey = "${plugin-resource-publickey}" def pluginResourceLicense = "${plugin-resource-license}" def pluginDeclaredVersion = "${plugin-declared-version}" def pluginId = "${plugin-fullname}" - override def getNumberOfNodes: Int = nodeInfoService.getNumberOfManagedNodes.runNow + override def getNumberOfNodes: Int = nodeFactRepo.getNumberOfManagedNodes().runNow } diff --git a/openscap/src/main/scala/bootstrap/rudder/plugin/OpenscapPoliciesConf.scala b/openscap/src/main/scala/bootstrap/rudder/plugin/OpenscapPoliciesConf.scala index e1f82639d..29cd5ad95 100644 --- a/openscap/src/main/scala/bootstrap/rudder/plugin/OpenscapPoliciesConf.scala +++ b/openscap/src/main/scala/bootstrap/rudder/plugin/OpenscapPoliciesConf.scala @@ -98,7 +98,7 @@ object OpenscapPoliciesConf extends RudderPluginModule { throw new IllegalArgumentException(s"OpenSCAP sanitization file not found: ${POLICY_SANITIZATION_FILE}") } - lazy val pluginStatusService = new CheckRudderPluginEnableImpl(RudderConfig.nodeInfoService) + lazy val pluginStatusService = new CheckRudderPluginEnableImpl(RudderConfig.nodeFactRepository) lazy val pluginDef: OpenscapPoliciesPluginDef = new OpenscapPoliciesPluginDef(OpenscapPoliciesConf.pluginStatusService) @@ -106,7 +106,7 @@ object OpenscapPoliciesConf extends RudderPluginModule { lazy val reportSanitizer = new ReportSanitizer(POLICY_SANITIZATION_FILE) lazy val openScapReportReader = new OpenScapReportReader( - RudderConfig.nodeInfoService, + RudderConfig.nodeFactRepository, RudderConfig.roDirectiveRepository, getActiveTechniqueIds, RudderConfig.findExpectedReportRepository, diff --git a/openscap/src/main/scala/com/normation/plugins/openscappolicies/api/OpenScapApi.scala b/openscap/src/main/scala/com/normation/plugins/openscappolicies/api/OpenScapApi.scala index 3f939c3b7..433e31d1b 100644 --- a/openscap/src/main/scala/com/normation/plugins/openscappolicies/api/OpenScapApi.scala +++ b/openscap/src/main/scala/com/normation/plugins/openscappolicies/api/OpenScapApi.scala @@ -84,7 +84,7 @@ class OpenScapApiImpl( authzToken: AuthzToken ): LiftResponse = { (for { - report <- openScapReportReader.getOpenScapReport(NodeId(nodeId)) + report <- openScapReportReader.getOpenScapReport(NodeId(nodeId))(authzToken.qc) } yield { logger.info(s"Report for node ${nodeId} has been found ") report @@ -127,7 +127,9 @@ class OpenScapApiImpl( authzToken: AuthzToken ): LiftResponse = { (for { - report <- openScapReportReader.getOpenScapReport(NodeId(nodeId)) ?~! s"Cannot get OpenSCAP report for node ${nodeId}" + report <- openScapReportReader.getOpenScapReport(NodeId(nodeId))( + authzToken.qc + ) ?~! s"Cannot get OpenSCAP report for node ${nodeId}" existence <- Box(report) ?~! s"Report not found for node ${nodeId}" sanitizedXml <- reportSanitizer.sanitizeReport(existence).toBox ?~! "Error while sanitizing report" } yield { diff --git a/openscap/src/main/scala/com/normation/plugins/openscappolicies/extension/OpenScapNodeDetailsExtension.scala b/openscap/src/main/scala/com/normation/plugins/openscappolicies/extension/OpenScapNodeDetailsExtension.scala index 92c614ed6..aa72a167c 100644 --- a/openscap/src/main/scala/com/normation/plugins/openscappolicies/extension/OpenScapNodeDetailsExtension.scala +++ b/openscap/src/main/scala/com/normation/plugins/openscappolicies/extension/OpenScapNodeDetailsExtension.scala @@ -5,12 +5,15 @@ import com.normation.plugins.PluginExtensionPoint import com.normation.plugins.PluginStatus import com.normation.plugins.openscappolicies.services.OpenScapReportReader import com.normation.plugins.openscappolicies.services.ReportSanitizer +import com.normation.rudder.users.CurrentUser import com.normation.rudder.web.components.ShowNodeDetailsFromNode + import net.liftweb.common.EmptyBox import net.liftweb.common.Full import net.liftweb.common.Loggable import net.liftweb.util.CssSel import net.liftweb.util.Helpers.* + import scala.reflect.ClassTag import scala.xml.NodeSeq @@ -34,7 +37,7 @@ class OpenScapNodeDetailsExtension( // Actually extend def display(): NodeSeq = { val nodeId = snippet.nodeId - val content = openScapReader.checkOpenScapReportExistence(nodeId) match { + val content = openScapReader.checkOpenScapReportExistence(nodeId)(CurrentUser.queryContext) match { case eb: EmptyBox => val e = eb ?~! "Can not display OpenSCAP report for that node" (
diff --git a/openscap/src/main/scala/com/normation/plugins/openscappolicies/services/OpenScapReportReader.scala b/openscap/src/main/scala/com/normation/plugins/openscappolicies/services/OpenScapReportReader.scala index e5b7dea1e..6c5fc6209 100644 --- a/openscap/src/main/scala/com/normation/plugins/openscappolicies/services/OpenScapReportReader.scala +++ b/openscap/src/main/scala/com/normation/plugins/openscappolicies/services/OpenScapReportReader.scala @@ -6,9 +6,10 @@ import com.normation.cfclerk.domain.TechniqueName import com.normation.inventory.domain.NodeId import com.normation.plugins.openscappolicies.OpenscapPoliciesLogger import com.normation.plugins.openscappolicies.OpenScapReport +import com.normation.rudder.facts.nodes.NodeFactRepository +import com.normation.rudder.facts.nodes.QueryContext import com.normation.rudder.repository.FindExpectedReportRepository import com.normation.rudder.repository.RoDirectiveRepository -import com.normation.rudder.services.nodes.NodeInfoService import net.liftweb.common.* import net.liftweb.util.Helpers.tryo @@ -17,7 +18,7 @@ import net.liftweb.util.Helpers.tryo * It is n /var/rudder/shared-files/root/NodeId/openscap.html */ class OpenScapReportReader( - nodeInfoService: NodeInfoService, + nodeFactRepo: NodeFactRepository, directiveRepository: RoDirectiveRepository, pluginDirectiveRepository: GetActiveTechniqueIds, findExpectedReportRepository: FindExpectedReportRepository, @@ -82,8 +83,8 @@ class OpenScapReportReader( * Checks if the report exists, return True if exists, of False otherwise. * Everything else is a failure */ - def checkOpenScapReportExistence(nodeId: NodeId): Box[Boolean] = { - nodeInfoService.getNodeInfo(nodeId).toBox match { + def checkOpenScapReportExistence(nodeId: NodeId)(implicit qc: QueryContext): Box[Boolean] = { + nodeFactRepo.get(nodeId).toBox match { case t: EmptyBox => val errMessage = s"Node with id ${nodeId.value} does not exist" logger.error(errMessage) @@ -131,7 +132,7 @@ class OpenScapReportReader( * If the file is not there, fails * Used for API */ - def getOpenScapReport(nodeId: NodeId): Box[Option[OpenScapReport]] = { + def getOpenScapReport(nodeId: NodeId)(implicit qc: QueryContext): Box[Option[OpenScapReport]] = { val path = computePathFromNodeId(nodeId) for { reportExists <- checkOpenScapReportExistence(nodeId) diff --git a/openscap/src/test/scala/com/normation/plugins/openscappolicies/api/OpenScapApiTest.scala b/openscap/src/test/scala/com/normation/plugins/openscappolicies/api/OpenScapApiTest.scala index 109dcb895..8b0a54b7b 100644 --- a/openscap/src/test/scala/com/normation/plugins/openscappolicies/api/OpenScapApiTest.scala +++ b/openscap/src/test/scala/com/normation/plugins/openscappolicies/api/OpenScapApiTest.scala @@ -44,7 +44,7 @@ class OpenScapApiTest extends ZIOSpecDefault { val mockNodes = new MockNodes() val openScapReportReader = new OpenScapReportReader( - mockNodes.nodeInfoService, + mockNodes.nodeFactRepo, restTestSetUp.mockDirectives.directiveRepo, null, null, diff --git a/scale-out-relay/src/main/scala-templates/default/com/normation/plugins/scaleoutrelay/EnablePluginImpl.scala b/scale-out-relay/src/main/scala-templates/default/com/normation/plugins/scaleoutrelay/EnablePluginImpl.scala index f83ba80eb..dace4f9c5 100644 --- a/scale-out-relay/src/main/scala-templates/default/com/normation/plugins/scaleoutrelay/EnablePluginImpl.scala +++ b/scale-out-relay/src/main/scala-templates/default/com/normation/plugins/scaleoutrelay/EnablePluginImpl.scala @@ -38,6 +38,6 @@ package com.normation.plugins.scaleoutrelay import com.normation.plugins.PluginEnableImpl -import com.normation.rudder.services.nodes.NodeInfoService +import com.normation.rudder.facts.nodes.NodeFactRepository -final class CheckRudderPluginEnableImpl(nodeInfoService: NodeInfoService) extends PluginEnableImpl +final class CheckRudderPluginEnableImpl(nodeFactRepo: NodeFactRepository) extends PluginEnableImpl diff --git a/scale-out-relay/src/main/scala-templates/limited/com/normation/plugins/scaleoutrelay/EnablePluginImpl.scala b/scale-out-relay/src/main/scala-templates/limited/com/normation/plugins/scaleoutrelay/EnablePluginImpl.scala index 023c395d3..5ac9b311f 100644 --- a/scale-out-relay/src/main/scala-templates/limited/com/normation/plugins/scaleoutrelay/EnablePluginImpl.scala +++ b/scale-out-relay/src/main/scala-templates/limited/com/normation/plugins/scaleoutrelay/EnablePluginImpl.scala @@ -38,7 +38,7 @@ package com.normation.plugins.scaleoutrelay import com.normation.plugins.LicensedPluginCheck -import com.normation.rudder.services.nodes.NodeInfoService +import com.normation.rudder.facts.nodes.NodeFactRepository import com.normation.zio.* /* * This template file will processed at build time to choose @@ -47,12 +47,12 @@ import com.normation.zio.* * * The class will be loaded by ServiceLoader, it needs an empty constructor. */ -final class CheckRudderPluginEnableImpl(nodeInfoService: NodeInfoService) extends LicensedPluginCheck { +final class CheckRudderPluginEnableImpl(nodeFactRepo: NodeFactRepository) extends LicensedPluginCheck { // here are processed variables def pluginResourcePublickey = "${plugin-resource-publickey}" def pluginResourceLicense = "${plugin-resource-license}" def pluginDeclaredVersion = "${plugin-declared-version}" def pluginId = "${plugin-fullname}" - override def getNumberOfNodes: Int = nodeInfoService.getNumberOfManagedNodes.runNow + override def getNumberOfNodes: Int = nodeFactRepo.getNumberOfManagedNodes().runNow } diff --git a/scale-out-relay/src/main/scala/bootstrap/rudder/plugin/ScaleOutRelayConf.scala b/scale-out-relay/src/main/scala/bootstrap/rudder/plugin/ScaleOutRelayConf.scala index 9f8964289..25e52bd98 100644 --- a/scale-out-relay/src/main/scala/bootstrap/rudder/plugin/ScaleOutRelayConf.scala +++ b/scale-out-relay/src/main/scala/bootstrap/rudder/plugin/ScaleOutRelayConf.scala @@ -52,7 +52,7 @@ import com.normation.plugins.scaleoutrelay.api.ScaleOutRelayApiImpl object ScalaOutRelayConf extends RudderPluginModule { // by build convention, we have only one of that on the classpath - lazy val pluginStatusService = new CheckRudderPluginEnableImpl(RudderConfig.nodeInfoService) + lazy val pluginStatusService = new CheckRudderPluginEnableImpl(RudderConfig.nodeFactRepository) override lazy val pluginDef: ScalaOutRelayPluginDef = new ScalaOutRelayPluginDef(ScalaOutRelayConf.pluginStatusService) From 6ff3c177a166c39eacad49d0ecf98d04d62afc60 Mon Sep 17 00:00:00 2001 From: Clark Andrianasolo Date: Mon, 4 Nov 2024 10:36:45 +0100 Subject: [PATCH 04/65] Fixes #25769: Update scala plugin dependencies --- auth-backends/pom-template.xml | 4 ++-- change-validation/pom-template.xml | 4 ++-- openscap/pom-template.xml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/auth-backends/pom-template.xml b/auth-backends/pom-template.xml index dd70231ce..49824cc20 100644 --- a/auth-backends/pom-template.xml +++ b/auth-backends/pom-template.xml @@ -71,12 +71,12 @@ com.fasterxml.jackson.core jackson-databind - 2.17.0 + 2.18.1 com.nimbusds nimbus-jose-jwt - 9.37.3 + 9.45 net.minidev diff --git a/change-validation/pom-template.xml b/change-validation/pom-template.xml index 61de0fadc..a2e4527a6 100644 --- a/change-validation/pom-template.xml +++ b/change-validation/pom-template.xml @@ -51,7 +51,7 @@ com.github.spullara.mustache.java compiler - 0.9.11 + 0.9.14 com.sun.mail @@ -63,7 +63,7 @@ com.github.davidmoten subethasmtp - 7.0.2 + 7.1.1 test diff --git a/openscap/pom-template.xml b/openscap/pom-template.xml index 5a6e6dd43..30cfe73a4 100644 --- a/openscap/pom-template.xml +++ b/openscap/pom-template.xml @@ -47,7 +47,7 @@ org.owasp.antisamy antisamy - 1.7.5 + 1.7.6 slf4j-simple From e1563a4a927f213b48c85ef0f296243d53a0de2b Mon Sep 17 00:00:00 2001 From: Clark Andrianasolo Date: Thu, 14 Nov 2024 15:58:04 +0100 Subject: [PATCH 05/65] Fixes #25874: Remove NodeInfoService impact on auth-backends --- .../com/normation/plugins/authbackends/EnablePluginImpl.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth-backends/src/main/scala-templates/limited/com/normation/plugins/authbackends/EnablePluginImpl.scala b/auth-backends/src/main/scala-templates/limited/com/normation/plugins/authbackends/EnablePluginImpl.scala index 0eead45b1..79adc8412 100644 --- a/auth-backends/src/main/scala-templates/limited/com/normation/plugins/authbackends/EnablePluginImpl.scala +++ b/auth-backends/src/main/scala-templates/limited/com/normation/plugins/authbackends/EnablePluginImpl.scala @@ -55,5 +55,5 @@ final class CheckRudderPluginEnableImpl(nodeFactRepo: NodeFactRepository) extend def pluginDeclaredVersion = "${plugin-declared-version}" def pluginId = "${plugin-fullname}" - override def getNumberOfNodes: Int = nodeFactRepo.getNumberOfManagedNodes.runNow + override def getNumberOfNodes: Int = nodeFactRepo.getNumberOfManagedNodes().runNow } From a44e48455ee2214714fcdb8ee26dac3adad16089 Mon Sep 17 00:00:00 2001 From: Alexis Mousset Date: Mon, 18 Nov 2024 16:03:10 +0100 Subject: [PATCH 06/65] Fixes #25891: Remove the api-client dependency fro; OpenSCAP plugin --- openscap/README.adoc | 2 +- openscap/packaging/metadata | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/openscap/README.adoc b/openscap/README.adoc index 75e36d7c5..31716522a 100644 --- a/openscap/README.adoc +++ b/openscap/README.adoc @@ -16,7 +16,7 @@ The plugin provides a Technique to execute the OpenSCAP tool at regular interval == Installation -* Your Rudder server must have python-requests and rudder-api-client installed +* Your Rudder server must have python-requests installed * Install the plugin on the Rudder Server with `rudder package install openscap` == Usage diff --git a/openscap/packaging/metadata b/openscap/packaging/metadata index f792bf482..b27b55bf7 100644 --- a/openscap/packaging/metadata +++ b/openscap/packaging/metadata @@ -5,10 +5,6 @@ "version": "${plugin-version}", "build-date": "${maven.build.timestamp}", "build-commit": "${commit-id}", - "depends": { - "apt": [ "rudder-api-client" ], - "rpm": [ "rudder-api-client" ] - }, "jar-files": [ "/opt/rudder/share/plugins/${plugin-name}/${plugin-name}/${plugin-name}.jar" ], "content": { "files.txz": "/opt/rudder/share/plugins/${plugin-name}/" From eb3ace21020004eb0e42c20619f9e31a48321124 Mon Sep 17 00:00:00 2001 From: Clark Andrianasolo Date: Wed, 27 Nov 2024 10:38:26 +0100 Subject: [PATCH 07/65] Fixes #25961: Impact of removing rest extractor lift-json methods on plugins --- .../api/ChangeRequestApi.scala | 32 +++++-------------- .../snippet/ChangeRequestChangesForm.scala | 5 +-- .../snippet/ChangeRequestDetails.scala | 4 +-- .../changevalidation/MockServices.scala | 4 +-- .../api/ChangeRequestApiTest.scala | 4 +-- 5 files changed, 17 insertions(+), 32 deletions(-) diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/api/ChangeRequestApi.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/api/ChangeRequestApi.scala index d515e5455..37974d390 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/api/ChangeRequestApi.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/api/ChangeRequestApi.scala @@ -53,6 +53,8 @@ import com.normation.rudder.api.ApiVersion import com.normation.rudder.api.HttpAction.DELETE import com.normation.rudder.api.HttpAction.GET import com.normation.rudder.api.HttpAction.POST +import com.normation.rudder.config.ReasonBehavior +import com.normation.rudder.config.UserPropertyService import com.normation.rudder.domain.nodes.NodeGroupUid import com.normation.rudder.domain.policies.DirectiveId import com.normation.rudder.domain.policies.DirectiveUid @@ -69,6 +71,7 @@ import com.normation.rudder.rest.EndpointSchema import com.normation.rudder.rest.EndpointSchema.syntax.* import com.normation.rudder.rest.GeneralApi import com.normation.rudder.rest.OneParam +import com.normation.rudder.rest.RudderJsonRequest.* import com.normation.rudder.rest.SortIndex import com.normation.rudder.rest.StartsAtVersion3 import com.normation.rudder.rest.ZeroParam @@ -82,8 +85,6 @@ import com.normation.rudder.services.modification.DiffService import com.normation.rudder.services.workflows.CommitAndDeployChangeRequestService import com.normation.rudder.services.workflows.WorkflowLevelService import com.normation.rudder.users.UserService -import com.normation.rudder.web.services.ReasonBehavior -import com.normation.rudder.web.services.UserPropertyService import enumeratum.* import net.liftweb.common.Box import net.liftweb.http.LiftResponse @@ -172,7 +173,9 @@ class ChangeRequestApiImpl( userService: UserService ) extends LiftApiModuleProvider[ChangeRequestApi] { import com.normation.plugins.changevalidation.api.ChangeRequestApi as API - implicit private val diffServiceImpl: DiffService = diffService + + implicit def reasonBehavior: ReasonBehavior = userPropertyService.reasonsFieldBehavior + implicit private val diffServiceImpl: DiffService = diffService override def schemas: ApiModuleProvider[ChangeRequestApi] = API @@ -302,7 +305,7 @@ class ChangeRequestApiImpl( s"Could not decline ChangeRequest ${id} details cause is: could not decline ChangeRequest ${id}, because status '${step.value}' cannot be cancelled." ) (_, func) = stepFunc - reason <- extractReason(req) + reason <- extractReason(req).toIO result <- func(changeRequest.id, authzToken.qc.actor, reason).toIO serialized <- serialize(changeRequest, result).toIO } yield { @@ -339,7 +342,7 @@ class ChangeRequestApiImpl( s"Could not accept ChangeRequest ${id} details cause is: you could not send Change Request from '${step.value}' to '${targetStep.value}'." ) (_, func) = stepFunc - reason <- extractReason(req) + reason <- extractReason(req).toIO result <- func(changeRequest.id, authzToken.qc.actor, reason).toIO serialized <- serialize(changeRequest, result).toIO } yield { @@ -513,25 +516,6 @@ class ChangeRequestApiImpl( ) } - private def extractReason(req: Req): IOResult[Option[String]] = { - import ReasonBehavior.* - (userPropertyService.reasonsFieldBehavior match { - case Disabled => ZIO.none - case mode => - val reason = req.params.get("reason").flatMap(_.headOption) - (mode: @unchecked) match { - case Mandatory => - reason - .notOptional("Reason field is mandatory and should be at least 5 characters long") - .reject { - case s if s.lengthIs < 5 => Inconsistency("Reason field should be at least 5 characters long") - } - .map(Some(_)) - case Optionnal => reason.succeed - } - }).chainError("There was an error while extracting reason message") - } - private[this] def extractFilters(params: Map[String, List[String]]): PureResult[ChangeRequestFilter] = { import ChangeRequestFilter.* for { diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestChangesForm.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestChangesForm.scala index 49834a631..9f4b8e988 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestChangesForm.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestChangesForm.scala @@ -75,6 +75,7 @@ import net.liftweb.util.Helpers.* import org.apache.commons.text.StringEscapeUtils import org.joda.time.DateTime import scala.xml.* +import zio.json.* object ChangeRequestChangesForm { def form = ChooseTemplate( @@ -442,7 +443,7 @@ class ChangeRequestChangesForm( "#shortDescription" #> group.description & "#query" #> (group.query match { case None => Text("None") - case Some(q) => Text(q.toJSONString) + case Some(q) => Text(q.toJson) }) & "#isDynamic" #> group.isDynamic & "#properties" #>
    {group.properties.map(p =>
  • {p.name}: {p.valueAsString}
  • )}
& @@ -467,7 +468,7 @@ class ChangeRequestChangesForm( )(implicit qc: QueryContext) = { def displayQuery(query: Option[Query]) = query match { case None => "None" - case Some(q) => q.toJSONString + case Some(q) => q.toJson } def displayServerList(servers: Set[NodeId]): String = { servers.map(_.value).toList.sortBy(s => s).mkString("\n") diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestDetails.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestDetails.scala index ec153093d..765cfb1af 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestDetails.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestDetails.scala @@ -335,11 +335,11 @@ class ChangeRequestDetails extends DispatchSnippet with Loggable { } val changeMessage = { - import com.normation.rudder.web.services.ReasonBehavior.* + import com.normation.rudder.config.ReasonBehavior.* userPropertyService.reasonsFieldBehavior match { case Disabled => None case Mandatory => Some(buildReasonField(true, "subContainerReasonField")) - case Optionnal => Some(buildReasonField(false, "subContainerReasonField")) + case Optional => Some(buildReasonField(false, "subContainerReasonField")) // for non-exhaustiveness God - yes, enum were not very well designed before scala 3 case x => throw new IllegalArgumentException(s"This case should not happen, please report to developers") } diff --git a/change-validation/src/test/scala/com/normation/plugins/changevalidation/MockServices.scala b/change-validation/src/test/scala/com/normation/plugins/changevalidation/MockServices.scala index 4457b5b02..aef9e2046 100644 --- a/change-validation/src/test/scala/com/normation/plugins/changevalidation/MockServices.scala +++ b/change-validation/src/test/scala/com/normation/plugins/changevalidation/MockServices.scala @@ -44,6 +44,8 @@ import com.normation.eventlog.ModificationId import com.normation.inventory.domain.NodeId import com.normation.rudder.AuthorizationType import com.normation.rudder.api.ApiAuthorization +import com.normation.rudder.config.StatelessUserPropertyService +import com.normation.rudder.config.UserPropertyService import com.normation.rudder.domain.eventlog.ChangeRequestDiff import com.normation.rudder.domain.eventlog.ChangeRequestEventLog import com.normation.rudder.domain.eventlog.WorkflowStepChanged @@ -71,8 +73,6 @@ import com.normation.rudder.services.workflows.CommitAndDeployChangeRequestServi import com.normation.rudder.users.AuthenticatedUser import com.normation.rudder.users.RudderAccount import com.normation.rudder.users.UserService -import com.normation.rudder.web.services.StatelessUserPropertyService -import com.normation.rudder.web.services.UserPropertyService import com.normation.zio.UnsafeRun import net.liftweb.common.Box import net.liftweb.common.Full diff --git a/change-validation/src/test/scala/com/normation/plugins/changevalidation/api/ChangeRequestApiTest.scala b/change-validation/src/test/scala/com/normation/plugins/changevalidation/api/ChangeRequestApiTest.scala index 859b7337f..c0fc625ea 100644 --- a/change-validation/src/test/scala/com/normation/plugins/changevalidation/api/ChangeRequestApiTest.scala +++ b/change-validation/src/test/scala/com/normation/plugins/changevalidation/api/ChangeRequestApiTest.scala @@ -73,9 +73,9 @@ import com.normation.rudder.domain.policies.Tags import com.normation.rudder.domain.properties.AddGlobalParameterDiff import com.normation.rudder.domain.properties.DeleteGlobalParameterDiff import com.normation.rudder.domain.properties.ModifyToGlobalParameterDiff -import com.normation.rudder.domain.queries.And -import com.normation.rudder.domain.queries.NodeReturnType +import com.normation.rudder.domain.queries.CriterionComposition.* import com.normation.rudder.domain.queries.Query +import com.normation.rudder.domain.queries.QueryReturnType.* import com.normation.rudder.domain.queries.ResultTransformation import com.normation.rudder.domain.workflows.ChangeRequestId import com.normation.rudder.domain.workflows.ChangeRequestInfo From 92bb3d90303da008642ba44caf3eaff83898169b Mon Sep 17 00:00:00 2001 From: Raphael Gauthier Date: Tue, 10 Dec 2024 16:22:16 +0100 Subject: [PATCH 08/65] Fixes #26048: Update front-end dependencies --- plugins-common/package-lock.json | 2062 ++++++++++++++++++------------ plugins-common/package.json | 8 +- 2 files changed, 1231 insertions(+), 839 deletions(-) diff --git a/plugins-common/package-lock.json b/plugins-common/package-lock.json index 00dcd81fb..fadf82796 100644 --- a/plugins-common/package-lock.json +++ b/plugins-common/package-lock.json @@ -6,13 +6,13 @@ "": { "dependencies": { "elm-review": "^2.11.2", - "gulp-sass": "^5.1.0", - "gulp-sourcemaps": "^3.0.0", - "sass": "^1.77.1" + "gulp-sass": "^6.0.0", + "gulp-sourcemaps": "^2.6.5", + "sass": "^1.77.6" }, "devDependencies": { "better-npm-audit": "^3.7.3", - "del": "^7.1.0", + "del": "^8.0.0", "elm": "^0.19.1-6", "gulp": "^5.0.0", "gulp-elm": "^0.8.2", @@ -23,6 +23,32 @@ "through2": "^4.0.2" } }, + "node_modules/@elm_binaries/darwin_arm64": { + "version": "0.19.1-0", + "resolved": "https://registry.npmjs.org/@elm_binaries/darwin_arm64/-/darwin_arm64-0.19.1-0.tgz", + "integrity": "sha512-mjbsH7BNHEAmoE2SCJFcfk5fIHwFIpxtSgnEAqMsVLpBUFoEtAeX+LQ+N0vSFJB3WAh73+QYx/xSluxxLcL6dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@elm_binaries/darwin_x64": { + "version": "0.19.1-0", + "resolved": "https://registry.npmjs.org/@elm_binaries/darwin_x64/-/darwin_x64-0.19.1-0.tgz", + "integrity": "sha512-QGUtrZTPBzaxgi9al6nr+9313wrnUVHuijzUK39UsPS+pa+n6CmWyV/69sHZeX9qy6UfeugE0PzF3qcUiy2GDQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, "node_modules/@elm_binaries/linux_x64": { "version": "0.19.1-0", "resolved": "https://registry.npmjs.org/@elm_binaries/linux_x64/-/linux_x64-0.19.1-0.tgz", @@ -36,47 +62,79 @@ "linux" ] }, + "node_modules/@elm_binaries/win32_x64": { + "version": "0.19.1-0", + "resolved": "https://registry.npmjs.org/@elm_binaries/win32_x64/-/win32_x64-0.19.1-0.tgz", + "integrity": "sha512-yDleiXqSE9EcqKtd9SkC/4RIW8I71YsXzMPL79ub2bBPHjWTcoyyeBbYjoOB9SxSlArJ74HaoBApzT6hY7Zobg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@gulp-sourcemaps/identity-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-2.0.1.tgz", - "integrity": "sha512-Tb+nSISZku+eQ4X1lAkevcQa+jknn/OVUgZ3XCxEKIsLsqYuPoJwJOPQeaOk75X3WPftb29GWY1eqE7GLsXb1Q==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-1.0.2.tgz", + "integrity": "sha512-ciiioYMLdo16ShmfHBXJBOFm3xPC4AuwO4xeRpFeHz7WK9PYsWCmigagG2XyzZpubK4a3qNKoUBDhbzHfa50LQ==", "dependencies": { - "acorn": "^6.4.1", - "normalize-path": "^3.0.0", - "postcss": "^7.0.16", + "acorn": "^5.0.3", + "css": "^2.2.1", + "normalize-path": "^2.1.1", "source-map": "^0.6.0", - "through2": "^3.0.1" + "through2": "^2.0.3" }, "engines": { "node": ">= 0.10" } }, - "node_modules/@gulp-sourcemaps/identity-map/node_modules/acorn": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", - "bin": { - "acorn": "bin/acorn" + "node_modules/@gulp-sourcemaps/identity-map/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dependencies": { + "remove-trailing-separator": "^1.0.1" }, "engines": { - "node": ">=0.4.0" + "node": ">=0.10.0" } }, - "node_modules/@gulp-sourcemaps/identity-map/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" + "node_modules/@gulp-sourcemaps/identity-map/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/@gulp-sourcemaps/identity-map/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/@gulp-sourcemaps/identity-map/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" } }, "node_modules/@gulp-sourcemaps/identity-map/node_modules/through2": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", - "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "2 || 3" + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" } }, "node_modules/@gulp-sourcemaps/map-sources": { @@ -102,6 +160,33 @@ "node": ">=0.10.0" } }, + "node_modules/@gulp-sourcemaps/map-sources/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/@gulp-sourcemaps/map-sources/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/@gulp-sourcemaps/map-sources/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/@gulp-sourcemaps/map-sources/node_modules/through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -149,9 +234,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "engines": { "node": ">=12" }, @@ -222,57 +307,57 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", - "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", - "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -283,7 +368,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -296,7 +380,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "engines": { "node": ">= 8" } @@ -305,7 +388,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -314,6 +396,288 @@ "node": ">= 8" } }, + "node_modules/@parcel/watcher": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", + "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.0", + "@parcel/watcher-darwin-arm64": "2.5.0", + "@parcel/watcher-darwin-x64": "2.5.0", + "@parcel/watcher-freebsd-x64": "2.5.0", + "@parcel/watcher-linux-arm-glibc": "2.5.0", + "@parcel/watcher-linux-arm-musl": "2.5.0", + "@parcel/watcher-linux-arm64-glibc": "2.5.0", + "@parcel/watcher-linux-arm64-musl": "2.5.0", + "@parcel/watcher-linux-x64-glibc": "2.5.0", + "@parcel/watcher-linux-x64-musl": "2.5.0", + "@parcel/watcher-win32-arm64": "2.5.0", + "@parcel/watcher-win32-ia32": "2.5.0", + "@parcel/watcher-win32-x64": "2.5.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", + "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", + "integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", + "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", + "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", + "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", + "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", + "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", + "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", + "integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", + "integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", + "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", + "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", + "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -334,6 +698,18 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", @@ -370,11 +746,11 @@ } }, "node_modules/@types/node": { - "version": "20.12.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", - "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", + "version": "22.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", + "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, "node_modules/@types/responselike": { @@ -383,46 +759,29 @@ "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", "dependencies": { "@types/node": "*" - } - }, - "node_modules/acorn": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", - "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/aggregate-error": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-4.0.1.tgz", - "integrity": "sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==", - "dev": true, - "dependencies": { - "clean-stack": "^4.0.0", - "indent-string": "^5.0.0" + } + }, + "node_modules/acorn": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", + "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==", + "bin": { + "acorn": "bin/acorn" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.4.0" } }, "node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -430,14 +789,12 @@ } }, "node_modules/ansi-colors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", - "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", - "dependencies": { - "ansi-wrap": "^0.1.0" - }, + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, "node_modules/ansi-escapes": { @@ -604,6 +961,12 @@ "node": ">= 4.5.0" } }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "dev": true + }, "node_modules/bach": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/bach/-/bach-2.0.1.tgz", @@ -624,9 +987,9 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/bare-events": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.2.tgz", - "integrity": "sha512-h7z00dWdG0PYOQEvChhOSWvOfkIKsdZGkWr083FgN/HyoQuebSew/cgirYqh9SCuy/hRvxc5Vy6Fw8xAmYHLkQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz", + "integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==", "dev": true, "optional": true }, @@ -650,14 +1013,15 @@ ] }, "node_modules/better-npm-audit": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/better-npm-audit/-/better-npm-audit-3.7.3.tgz", - "integrity": "sha512-zsSiidlP5n7KpCYdAmkellu4JYA4IoRUUwrBMv/R7TwT8vcRfk5CQ2zTg7yUy4bdWkKtAj7VVdPQttdMbx+n5Q==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/better-npm-audit/-/better-npm-audit-3.11.0.tgz", + "integrity": "sha512-/Pt05DK6HQaRjWDc5McsCkJBZYfhgQGneKnxzPJExtRq38NttO1Hm30m0GVQeZogE94LVNBVrhWwVsoCo+at3g==", "dev": true, "dependencies": { "commander": "^8.0.0", "dayjs": "^1.10.6", "lodash.get": "^4.4.2", + "semver": "^7.6.3", "table": "^6.7.1" }, "bin": { @@ -679,30 +1043,15 @@ } }, "node_modules/bl": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", - "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", - "dev": true, + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dependencies": { - "buffer": "^6.0.3", + "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, - "node_modules/bl/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -713,21 +1062,20 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" } }, "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "funding": [ { "type": "github", @@ -744,7 +1092,7 @@ ], "dependencies": { "base64-js": "^1.3.1", - "ieee754": "^1.2.1" + "ieee754": "^1.1.13" } }, "node_modules/buffer-from": { @@ -816,21 +1164,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/clean-stack": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-4.2.0.tgz", - "integrity": "sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==", - "dev": true, - "dependencies": { - "escape-string-regexp": "5.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -910,6 +1243,36 @@ "readable-stream": "^2.3.5" } }, + "node_modules/cloneable-readable/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/cloneable-readable/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/cloneable-readable/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -973,70 +1336,53 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, "node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">=4.8" + "node": ">= 8" } }, "node_modules/css": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", - "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz", + "integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==", "dependencies": { - "inherits": "^2.0.4", + "inherits": "^2.0.3", "source-map": "^0.6.1", - "source-map-resolve": "^0.6.0" - } - }, - "node_modules/css/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/css/node_modules/source-map-resolve": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", - "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", - "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", - "dependencies": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0" + "source-map-resolve": "^0.5.2", + "urix": "^0.1.0" } }, "node_modules/d": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", "dependencies": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" } }, "node_modules/dayjs": { - "version": "1.11.10", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", - "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==", + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", "dev": true }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1065,11 +1411,6 @@ "ms": "^2.1.1" } }, - "node_modules/debug-fabulous/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, "node_modules/decode-uri-component": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", @@ -1131,22 +1472,20 @@ } }, "node_modules/del": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/del/-/del-7.1.0.tgz", - "integrity": "sha512-v2KyNk7efxhlyHpjEvfyxaAihKKK0nWCuf6ZtqZcFFpQRG0bJ12Qsr0RpvsICMjAAZ8DOVCxrlqpxISlMHC4Kg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-8.0.0.tgz", + "integrity": "sha512-R6ep6JJ+eOBZsBr9esiNN1gxFbZE4Q2cULkUSFumGYecAiS6qodDvcPx/sFuWHMNul7DWmrtoEOpYSm7o6tbSA==", "dev": true, "dependencies": { - "globby": "^13.1.2", - "graceful-fs": "^4.2.10", + "globby": "^14.0.2", "is-glob": "^4.0.3", "is-path-cwd": "^3.0.0", "is-path-inside": "^4.0.0", - "p-map": "^5.5.0", - "rimraf": "^3.0.2", - "slash": "^4.0.0" + "p-map": "^7.0.2", + "slash": "^5.1.0" }, "engines": { - "node": ">=14.16" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -1161,6 +1500,18 @@ "node": ">=0.10.0" } }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/detect-newline": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", @@ -1173,7 +1524,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, "dependencies": { "path-type": "^4.0.0" }, @@ -1181,6 +1531,14 @@ "node": ">=8" } }, + "node_modules/dir-glob/node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } + }, "node_modules/each-props": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/each-props/-/each-props-3.0.0.tgz", @@ -1219,9 +1577,9 @@ } }, "node_modules/elm-review": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/elm-review/-/elm-review-2.11.2.tgz", - "integrity": "sha512-FhZ/m59C9CPWgn34/yvGUoCsSmXDnFFR2GfmuFbcnRwcn2aybsOUav5ET2lXs+4V1zZMRTG6In8cCUooAhwHVg==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/elm-review/-/elm-review-2.12.0.tgz", + "integrity": "sha512-so+G1hvCV85A63sQQzEhCNFNYyQVWDexXrz0TNEYg3/IIGHzN1bcRN+W4KJSvvFcmfEImzMSJ9AN20bvemU+4Q==", "dependencies": { "chalk": "^4.0.0", "chokidar": "^3.5.2", @@ -1231,7 +1589,8 @@ "find-up": "^4.1.0", "folder-hash": "^3.3.0", "fs-extra": "^9.0.0", - "glob": "^7.1.4", + "glob": "^10.2.6", + "globby": "^13.2.2", "got": "^11.8.5", "graceful-fs": "^4.2.11", "minimist": "^1.2.6", @@ -1254,118 +1613,33 @@ "url": "https://github.com/sponsors/jfmengels" } }, - "node_modules/elm-review/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/elm-review/node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/elm-review/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/elm-review/node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/elm-review/node_modules/rimraf": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz", - "integrity": "sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==", + "node_modules/elm-review/node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" }, "engines": { - "node": ">=14.18" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/elm-review/node_modules/rimraf/node_modules/glob": { - "version": "10.3.16", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.16.tgz", - "integrity": "sha512-JDKXl1DiuuHJ6fVS2FXjownaavciiHNUU4mOvV/B793RLh05vZL1rcPnCSaOgv1hDT6RDlY7AB7ZUvFYAtPgAw==", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.11.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, + "node_modules/elm-review/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", "engines": { - "node": ">=16 || 14 >=14.18" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/elm-review/node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/elm-review/node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "engines": { - "node": ">=8" - } - }, - "node_modules/elm-review/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/elm-solve-deps-wasm": { @@ -1412,12 +1686,15 @@ } }, "node_modules/es6-symbol": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", - "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", "dependencies": { - "d": "^1.0.1", - "ext": "^1.1.2" + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" } }, "node_modules/es6-weak-map": { @@ -1431,25 +1708,13 @@ "es6-symbol": "^3.1.1" } }, - "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6" } }, "node_modules/esniff": { @@ -1466,11 +1731,6 @@ "node": ">=0.10" } }, - "node_modules/esniff/node_modules/type": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", - "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" - }, "node_modules/event-emitter": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", @@ -1500,17 +1760,24 @@ "type": "^2.7.2" } }, - "node_modules/ext/node_modules/type": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", - "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "dev": true }, + "node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fancy-log": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", @@ -1542,7 +1809,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -1563,6 +1829,12 @@ "fastest-levenshtein": "^1.0.7" } }, + "node_modules/fast-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "dev": true + }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -1575,15 +1847,14 @@ "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, "dependencies": { "reusify": "^1.0.4" } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1659,17 +1930,6 @@ "node": ">=6.0.0" } }, - "node_modules/folder-hash/node_modules/minimatch": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", - "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -1692,9 +1952,9 @@ } }, "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -1706,71 +1966,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/foreground-child/node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/foreground-child/node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/foreground-child/node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/foreground-child/node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "engines": { - "node": ">=8" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -1801,7 +1996,21 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } }, "node_modules/function-bind": { "version": "1.1.2", @@ -1836,19 +2045,19 @@ } }, "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": "*" + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -1909,6 +2118,28 @@ "node": ">= 10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/global-modules": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", @@ -1939,20 +2170,33 @@ "node": ">=0.10.0" } }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/globby": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", - "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", "dev": true, "dependencies": { - "dir-glob": "^3.0.1", - "fast-glob": "^3.3.0", + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", "ignore": "^5.2.4", - "merge2": "^1.4.1", - "slash": "^4.0.0" + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2060,13 +2304,59 @@ "which": "^1.0.8" } }, - "node_modules/gulp-elm/node_modules/ansi-colors": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", - "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "node_modules/gulp-elm/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/gulp-elm/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", "dev": true, "engines": { - "node": ">=6" + "node": ">=4" + } + }, + "node_modules/gulp-elm/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/gulp-elm/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-elm/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, "node_modules/gulp-elm/node_modules/through2": { @@ -2079,6 +2369,18 @@ "readable-stream": "2 || 3" } }, + "node_modules/gulp-elm/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/gulp-mode": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/gulp-mode/-/gulp-mode-1.1.0.tgz", @@ -2111,6 +2413,36 @@ "node": ">=0.10" } }, + "node_modules/gulp-noop/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/gulp-noop/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/gulp-noop/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/gulp-noop/node_modules/through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -2131,9 +2463,9 @@ } }, "node_modules/gulp-sass": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/gulp-sass/-/gulp-sass-5.1.0.tgz", - "integrity": "sha512-7VT0uaF+VZCmkNBglfe1b34bxn/AfcssquLKVDYnCDJ3xNBaW7cUuI3p3BQmoKcoKFrs9jdzUxyb+u+NGfL4OQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gulp-sass/-/gulp-sass-6.0.0.tgz", + "integrity": "sha512-FGb4Uab4jnH2GnSfBGd6uW3+imvNodAGfsjGcUhEtpNYPVx+TK2tp5uh7MO0sSR7aIf1Sm544werc+zV7ejHHw==", "dependencies": { "lodash.clonedeep": "^4.5.0", "picocolors": "^1.0.0", @@ -2146,52 +2478,52 @@ "node": ">=12" } }, - "node_modules/gulp-sass/node_modules/replace-ext": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", - "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", - "engines": { - "node": ">= 10" - } - }, - "node_modules/gulp-sourcemaps": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-3.0.0.tgz", - "integrity": "sha512-RqvUckJkuYqy4VaIH60RMal4ZtG0IbQ6PXMNkNsshEGJ9cldUPRb/YCgboYae+CLAs1HQNb4ADTKCx65HInquQ==", - "dependencies": { - "@gulp-sourcemaps/identity-map": "^2.0.1", - "@gulp-sourcemaps/map-sources": "^1.0.0", - "acorn": "^6.4.1", - "convert-source-map": "^1.0.0", - "css": "^3.0.0", - "debug-fabulous": "^1.0.0", - "detect-newline": "^2.0.0", - "graceful-fs": "^4.0.0", - "source-map": "^0.6.0", - "strip-bom-string": "^1.0.0", - "through2": "^2.0.0" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/gulp-sourcemaps/node_modules/acorn": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", - "bin": { - "acorn": "bin/acorn" + "node_modules/gulp-sourcemaps": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-2.6.5.tgz", + "integrity": "sha512-SYLBRzPTew8T5Suh2U8jCSDKY+4NARua4aqjj8HOysBh2tSgT9u4jc1FYirAdPx1akUxxDeK++fqw6Jg0LkQRg==", + "dependencies": { + "@gulp-sourcemaps/identity-map": "1.X", + "@gulp-sourcemaps/map-sources": "1.X", + "acorn": "5.X", + "convert-source-map": "1.X", + "css": "2.X", + "debug-fabulous": "1.X", + "detect-newline": "2.X", + "graceful-fs": "4.X", + "source-map": "~0.6.0", + "strip-bom-string": "1.X", + "through2": "2.X" }, "engines": { - "node": ">=0.4.0" + "node": ">=4" } }, - "node_modules/gulp-sourcemaps/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" + "node_modules/gulp-sourcemaps/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/gulp-sourcemaps/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/gulp-sourcemaps/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" } }, "node_modules/gulp-sourcemaps/node_modules/through2": { @@ -2311,35 +2643,24 @@ ] }, "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", - "dev": true, + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "engines": { "node": ">= 4" } }, "node_modules/immutable": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", - "integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==" - }, - "node_modules/indent-string": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", - "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", + "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==" }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -2390,17 +2711,42 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extendable/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2563,15 +2909,12 @@ } }, "node_modules/jackspeak": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.1.2.tgz", - "integrity": "sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dependencies": { "@isaacs/cliui": "^8.0.2" }, - "engines": { - "node": ">=14" - }, "funding": { "url": "https://github.com/sponsors/isaacs" }, @@ -2705,12 +3048,9 @@ } }, "node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", - "engines": { - "node": "14 || >=16.14" - } + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" }, "node_modules/lru-queue": { "version": "0.1.0", @@ -2730,18 +3070,21 @@ } }, "node_modules/memoizee": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", - "integrity": "sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==", + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.53", + "d": "^1.0.2", + "es5-ext": "^0.10.64", "es6-weak-map": "^2.0.3", "event-emitter": "^0.3.5", "is-promise": "^2.2.2", "lru-queue": "^0.1.0", "next-tick": "^1.1.0", "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.12" } }, "node_modules/merge-stream": { @@ -2754,18 +3097,16 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "engines": { "node": ">= 8" } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -2789,9 +3130,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2808,9 +3149,9 @@ } }, "node_modules/minipass": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.1.tgz", - "integrity": "sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "engines": { "node": ">=16 || 14 >=14.17" } @@ -2828,9 +3169,9 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/mute-stdout": { "version": "2.0.0", @@ -2852,6 +3193,12 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "optional": true + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -2962,52 +3309,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ora/node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/ora/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/ora/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/p-cancelable": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", @@ -3042,15 +3343,12 @@ } }, "node_modules/p-map": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-5.5.0.tgz", - "integrity": "sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", + "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", "dev": true, - "dependencies": { - "aggregate-error": "^4.0.0" - }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -3064,6 +3362,11 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" + }, "node_modules/parse-filepath": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", @@ -3108,17 +3411,17 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/path-parse": { @@ -3164,18 +3467,21 @@ } }, "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", "dev": true, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -3202,65 +3508,13 @@ "node": ">= 0.10" } }, - "node_modules/plugin-error/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/plugin-error/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/plugin-error/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "node_modules/plugin-error/node_modules/ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" + "ansi-wrap": "^0.1.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/postcss/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "engines": { "node": ">=0.10.0" } @@ -3283,27 +3537,19 @@ } }, "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", "dev": true, "engines": { "node": ">=0.6.0", @@ -3314,7 +3560,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -3348,17 +3593,16 @@ } }, "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" } }, "node_modules/readdirp": { @@ -3390,12 +3634,11 @@ "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==" }, "node_modules/replace-ext": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", - "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", - "dev": true, + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", "engines": { - "node": ">= 0.10" + "node": ">= 10" } }, "node_modules/replace-homedir": { @@ -3472,6 +3715,12 @@ "node": ">= 10.13.0" } }, + "node_modules/resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", + "deprecated": "https://github.com/lydell/resolve-url#deprecated" + }, "node_modules/responselike": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", @@ -3495,26 +3744,29 @@ "node": ">=8" } }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" } }, "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", "dependencies": { - "glob": "^7.1.3" + "glob": "^10.3.7" }, "bin": { - "rimraf": "bin.js" + "rimraf": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -3524,7 +3776,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -3544,9 +3795,23 @@ } }, "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, "node_modules/safer-buffer": { "version": "2.1.2", @@ -3555,12 +3820,12 @@ "dev": true }, "node_modules/sass": { - "version": "1.77.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.1.tgz", - "integrity": "sha512-OMEyfirt9XEfyvocduUIOlUSkWOXS/LAt6oblR/ISXCTukyavjex+zQNm51pPCOiFKY1QpWvEH1EeCkgyV3I6w==", + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.82.0.tgz", + "integrity": "sha512-j4GMCTa8elGyN9A7x7bEglx0VgSpNUG4W4wNedQ33wSMdnkqQCT8HTwOaVSV4e6yQovcu/3Oc4coJP/l0xhL2Q==", "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", + "chokidar": "^4.0.0", + "immutable": "^5.0.2", "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { @@ -3568,15 +3833,47 @@ }, "engines": { "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, "bin": { - "semver": "bin/semver" + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/semver-greatest-satisfied-range": { @@ -3592,30 +3889,34 @@ } }, "node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dependencies": { - "shebang-regex": "^1.0.0" + "shebang-regex": "^3.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/sisteransi": { "version": "1.0.5", @@ -3623,12 +3924,12 @@ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" }, "node_modules/slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", "dev": true, "engines": { - "node": ">=12" + "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -3652,21 +3953,34 @@ } }, "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "engines": { "node": ">=0.10.0" } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "engines": { "node": ">=0.10.0" } }, + "node_modules/source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -3677,14 +3991,11 @@ "source-map": "^0.6.0" } }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } + "node_modules/source-map-url": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", + "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", + "deprecated": "See https://github.com/lydell/source-map-url#deprecated" }, "node_modules/sparkles": { "version": "2.1.0", @@ -3711,24 +4022,25 @@ "dev": true }, "node_modules/streamx": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.16.1.tgz", - "integrity": "sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==", + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.21.0.tgz", + "integrity": "sha512-Qz6MsDZXJ6ur9u+b+4xCG18TluU7PGlRfXVAAjNiGsFrBUt/ioyLkxbFaKJygoPs+/kW4VyBj0bSj89Qu0IGyg==", "dev": true, "dependencies": { - "fast-fifo": "^1.1.0", - "queue-tick": "^1.0.1" + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" }, "optionalDependencies": { "bare-events": "^2.2.0" } }, "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dependencies": { - "safe-buffer": "~5.1.0" + "safe-buffer": "~5.2.0" } }, "node_modules/string-width": { @@ -3844,9 +4156,9 @@ } }, "node_modules/table": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", - "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", "dev": true, "dependencies": { "ajv": "^8.0.1", @@ -3881,10 +4193,44 @@ "node": ">=6.0.0" } }, + "node_modules/temp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/temp/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/temp/node_modules/rimraf": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "dependencies": { "glob": "^7.1.3" @@ -3909,9 +4255,9 @@ } }, "node_modules/terser": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.26.0.tgz", - "integrity": "sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==", + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", + "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -3926,12 +4272,33 @@ "node": ">=10" } }, + "node_modules/terser/node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, + "node_modules/text-decoder": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.2.tgz", + "integrity": "sha512-/MDslo7ZyWTA2vnk1j7XoDVfXsGk3tp+zFEJHJGm0UjIlQifonVFwlVbQDFh8KJzTBnT8ie115TYqir6bclddA==", + "dev": true, + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/through2": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", @@ -3941,20 +4308,6 @@ "readable-stream": "3" } }, - "node_modules/through2/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/time-stamp": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", @@ -3965,12 +4318,15 @@ } }, "node_modules/timers-ext": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz", - "integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==", + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", + "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", "dependencies": { - "es5-ext": "~0.10.46", - "next-tick": "1" + "es5-ext": "^0.10.64", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.12" } }, "node_modules/to-regex-range": { @@ -3997,9 +4353,9 @@ } }, "node_modules/type": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", - "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==" }, "node_modules/type-fest": { "version": "0.21.3", @@ -4046,9 +4402,21 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/universalify": { "version": "2.0.1", @@ -4058,14 +4426,11 @@ "node": ">= 10.0.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } + "node_modules/urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", + "deprecated": "Please see https://github.com/lydell/urix#deprecated" }, "node_modules/util-deprecate": { "version": "1.0.2", @@ -4120,13 +4485,39 @@ "node": ">=10.13.0" } }, - "node_modules/vinyl-contents/node_modules/replace-ext": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", - "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "node_modules/vinyl-contents/node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", "dev": true, - "engines": { - "node": ">= 10" + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/vinyl-contents/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" } }, "node_modules/vinyl-contents/node_modules/vinyl": { @@ -4170,15 +4561,6 @@ "node": ">=10.13.0" } }, - "node_modules/vinyl-fs/node_modules/replace-ext": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", - "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, "node_modules/vinyl-fs/node_modules/vinyl": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", @@ -4218,15 +4600,6 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, - "node_modules/vinyl-sourcemap/node_modules/replace-ext": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", - "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, "node_modules/vinyl-sourcemap/node_modules/vinyl": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", @@ -4251,6 +4624,23 @@ "source-map": "^0.5.1" } }, + "node_modules/vinyl-sourcemaps-apply/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vinyl/node_modules/replace-ext": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", + "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -4260,15 +4650,17 @@ } }, "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dependencies": { "isexe": "^2.0.0" }, "bin": { - "which": "bin/which" + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, "node_modules/wrap-ansi": { diff --git a/plugins-common/package.json b/plugins-common/package.json index 136f19498..99167204f 100644 --- a/plugins-common/package.json +++ b/plugins-common/package.json @@ -1,7 +1,7 @@ { "devDependencies": { "better-npm-audit": "^3.7.3", - "del": "^7.1.0", + "del": "^8.0.0", "elm": "^0.19.1-6", "gulp": "^5.0.0", "gulp-elm": "^0.8.2", @@ -13,8 +13,8 @@ }, "dependencies": { "elm-review": "^2.11.2", - "gulp-sass": "^5.1.0", - "gulp-sourcemaps": "^3.0.0", - "sass": "^1.77.1" + "gulp-sass": "^6.0.0", + "gulp-sourcemaps": "^2.6.5", + "sass": "^1.77.6" } } From 4f4c5993d78dcc1c6e866e2afd51afd36faf33f9 Mon Sep 17 00:00:00 2001 From: "Francois @fanf42 Armand" Date: Thu, 19 Dec 2024 08:20:18 +0100 Subject: [PATCH 09/65] Fixes #26094: Impact of JsDataLine in change validation --- .../changevalidation/snippet/ChangeRequestManagement.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestManagement.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestManagement.scala index e1afed8ee..f2f96c06b 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestManagement.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestManagement.scala @@ -55,6 +55,7 @@ import net.liftweb.http.SHtml import net.liftweb.http.SHtml.SelectableOption import net.liftweb.http.js.JE.* import net.liftweb.http.js.JsCmds.* +import net.liftweb.http.js.JsObj import net.liftweb.util.Helpers.* import org.apache.commons.text.StringEscapeUtils import scala.xml.Elem @@ -99,7 +100,9 @@ class ChangeRequestManagement extends DispatchSnippet with Loggable { val date = eventsMap.get(changeRequest.id).map(event => DateFormaterService.serialize(event.creationDate)).getOrElse("Unknown") - val json = { + override def json(freshName: () => String): JsObj = toJson + + val toJson = { JsObj( "id" -> changeRequest.id.value, "name" -> changeRequest.info.name, @@ -132,7 +135,7 @@ class ChangeRequestManagement extends DispatchSnippet with Loggable { } def dataTableInit = { val refresh = AnonFunc( - SHtml.ajaxInvoke(() => JsRaw(s"refreshTable('${changeRequestTableId}',${getLines().json.toJsCmd})")) + SHtml.ajaxInvoke(() => JsRaw(s"refreshTable('${changeRequestTableId}',${getLines().toJson.toJsCmd})")) ) // JsRaw ok, from json val filter = initFilter match { From ccc30cab1e4af067a28db8a76d2d339d96ee59d1 Mon Sep 17 00:00:00 2001 From: "Francois @fanf42 Armand" Date: Thu, 19 Dec 2024 14:41:46 +0100 Subject: [PATCH 10/65] Fixes #26095: Change main menu (public plugins) --- .../rudder/plugin/AuthBackendsConf.scala | 4 +- .../authbackends/AuthBackendsPluginDef.scala | 21 +------ .../template/brandingManagement.html | 15 ++--- .../rudder/plugin/BrandingPluginConf.scala | 2 + .../plugins/branding/BrandingConf.scala | 6 +- .../plugins/branding/BrandingPluginDef.scala | 22 -------- .../branding/snippet/BrandingResources.scala | 21 ++++--- .../branding/snippet/CommonBranding.scala | 11 ++-- .../branding/snippet/LoginBranding.scala | 3 +- change-validation/README.adoc | 4 +- .../template/ChangeValidationManagement.html | 41 +++++--------- .../rudder/plugin/ChangeValidationConf.scala | 2 + .../ChangeValidationPluginDef.scala | 35 +++--------- .../comet/WorkflowInformation.scala | 4 +- .../extension/ChangeValidationTab.scala | 55 +++++++++++++++++++ .../snippet/ChangeRequestDetails.scala | 4 +- .../emails/change-validation-email.conf | 6 +- .../changevalidation/TestEmailService.scala | 2 +- .../datasources/DataSourcesPluginDef.scala | 2 - .../NodeExternalReportPluginDef.scala | 2 - .../CreateNodeDetailsExtension.scala | 2 - 21 files changed, 125 insertions(+), 139 deletions(-) create mode 100644 change-validation/src/main/scala/com/normation/plugins/changevalidation/extension/ChangeValidationTab.scala diff --git a/auth-backends/src/main/scala/bootstrap/rudder/plugin/AuthBackendsConf.scala b/auth-backends/src/main/scala/bootstrap/rudder/plugin/AuthBackendsConf.scala index 0e9c803f3..1c779a99a 100644 --- a/auth-backends/src/main/scala/bootstrap/rudder/plugin/AuthBackendsConf.scala +++ b/auth-backends/src/main/scala/bootstrap/rudder/plugin/AuthBackendsConf.scala @@ -66,10 +66,10 @@ import com.normation.rudder.rest.RoleApiMapping import com.normation.rudder.users.* import com.normation.zio.* import com.typesafe.config.ConfigException -import java.net.URI -import java.util import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse +import java.net.URI +import java.util import org.joda.time.DateTime import org.springframework.context.ApplicationContext import org.springframework.context.ApplicationContextAware diff --git a/auth-backends/src/main/scala/com/normation/plugins/authbackends/AuthBackendsPluginDef.scala b/auth-backends/src/main/scala/com/normation/plugins/authbackends/AuthBackendsPluginDef.scala index 65f911333..54e934b60 100644 --- a/auth-backends/src/main/scala/com/normation/plugins/authbackends/AuthBackendsPluginDef.scala +++ b/auth-backends/src/main/scala/com/normation/plugins/authbackends/AuthBackendsPluginDef.scala @@ -37,18 +37,11 @@ package com.normation.plugins.authbackends -import bootstrap.liftweb.Boot import bootstrap.liftweb.ConfigResource import bootstrap.rudder.plugin.AuthBackendsConf import com.normation.plugins.* -import com.normation.rudder.AuthorizationType.Administration import com.normation.rudder.rest.EndpointSchema import com.normation.rudder.rest.lift.LiftApiModuleProvider -import net.liftweb.http.ClasspathTemplates -import net.liftweb.sitemap.Loc.LocGroup -import net.liftweb.sitemap.Loc.Template -import net.liftweb.sitemap.Loc.TestAccess -import net.liftweb.sitemap.LocPath.stringToLocPath import net.liftweb.sitemap.Menu class AuthBackendsPluginDef(override val status: PluginStatus) extends DefaultPluginDef { @@ -65,16 +58,6 @@ class AuthBackendsPluginDef(override val status: PluginStatus) extends DefaultPl override def apis: Option[LiftApiModuleProvider[? <: EndpointSchema]] = Some(AuthBackendsConf.api) - override def pluginMenuEntry: List[(Menu, Option[String])] = { - ( - (Menu("authBackensdManagement", Authentication backends) / - "secure" / "plugins" / "authBackendsManagement" - >> LocGroup("pluginsGroup") - >> TestAccess(() => Boot.userIsAllowed("/secure/index", Administration.Read)) - >> Template(() => - ClasspathTemplates("template" :: "AuthBackendsManagement" :: Nil) openOr
Template not found
- )).toMenu, - None - ) :: Nil - } + // no menu, the doc will be directly in plugin + override def pluginMenuEntry: List[(Menu, Option[String])] = Nil } diff --git a/branding/src/main/resources/template/brandingManagement.html b/branding/src/main/resources/template/brandingManagement.html index c135ad6b6..fc79de874 100755 --- a/branding/src/main/resources/template/brandingManagement.html +++ b/branding/src/main/resources/template/brandingManagement.html @@ -1,9 +1,10 @@ - - -
+ + + + + - Plugin Branding @@ -20,7 +21,7 @@ $('#headerBar > .background > span').text(settings.labelTxt); }); -
- - + + + diff --git a/branding/src/main/scala/bootstrap/rudder/plugin/BrandingPluginConf.scala b/branding/src/main/scala/bootstrap/rudder/plugin/BrandingPluginConf.scala index 7d493cb7b..4afed2110 100644 --- a/branding/src/main/scala/bootstrap/rudder/plugin/BrandingPluginConf.scala +++ b/branding/src/main/scala/bootstrap/rudder/plugin/BrandingPluginConf.scala @@ -44,6 +44,7 @@ import com.normation.plugins.branding.BrandingPluginDef import com.normation.plugins.branding.CheckRudderPluginEnableImpl import com.normation.plugins.branding.api.BrandingApi import com.normation.plugins.branding.api.BrandingApiService +import com.normation.plugins.branding.snippet.BrandingResources import com.normation.plugins.branding.snippet.CommonBranding import com.normation.plugins.branding.snippet.LoginBranding @@ -62,6 +63,7 @@ object BrandingPluginConf extends RudderPluginModule { new BrandingApi(brandingApiService, RudderConfig.stringUuidGenerator) RudderConfig.rudderApi.addModules(brandingApi.getLiftEndpoints()) + RudderConfig.snippetExtensionRegister.register(new BrandingResources(pluginDef.status)) RudderConfig.snippetExtensionRegister.register(new CommonBranding(pluginStatusService)) RudderConfig.snippetExtensionRegister.register(new LoginBranding(pluginStatusService, pluginDef.version)) } diff --git a/branding/src/main/scala/com/normation/plugins/branding/BrandingConf.scala b/branding/src/main/scala/com/normation/plugins/branding/BrandingConf.scala index cfed8c18f..b96f0dbce 100644 --- a/branding/src/main/scala/com/normation/plugins/branding/BrandingConf.scala +++ b/branding/src/main/scala/com/normation/plugins/branding/BrandingConf.scala @@ -105,7 +105,7 @@ object JsonColor { final case class Logo(enable: Boolean, name: Option[String], data: Option[String]) { def loginLogo = (enable, data) match { case (true, Some(d)) => () - case (_, _) => Rudder + case (_, _) => Rudder } def commonWideLogo = (enable, data) match { @@ -116,7 +116,7 @@ final case class Logo(enable: Boolean, name: Option[String], data: Option[String .sidebar-collapse .logo-lg{{display: none !important;}} - case (_, _) => Rudder + case (_, _) => Rudder } def commonSmallLogo = (enable, data) match { case (true, Some(d)) => @@ -133,7 +133,7 @@ final case class Logo(enable: Boolean, name: Option[String], data: Option[String }} - case (_, _) => Rudder + case (_, _) => Rudder } } diff --git a/branding/src/main/scala/com/normation/plugins/branding/BrandingPluginDef.scala b/branding/src/main/scala/com/normation/plugins/branding/BrandingPluginDef.scala index 0abea7400..6d688229e 100644 --- a/branding/src/main/scala/com/normation/plugins/branding/BrandingPluginDef.scala +++ b/branding/src/main/scala/com/normation/plugins/branding/BrandingPluginDef.scala @@ -37,21 +37,12 @@ package com.normation.plugins.branding -import bootstrap.liftweb.Boot import bootstrap.liftweb.ConfigResource -import bootstrap.liftweb.MenuUtils import bootstrap.rudder.plugin.BrandingPluginConf import com.normation.plugins.DefaultPluginDef import com.normation.plugins.PluginStatus -import com.normation.rudder.AuthorizationType.Administration import com.normation.rudder.rest.EndpointSchema import com.normation.rudder.rest.lift.LiftApiModuleProvider -import net.liftweb.http.ClasspathTemplates -import net.liftweb.sitemap.Loc.LocGroup -import net.liftweb.sitemap.Loc.Template -import net.liftweb.sitemap.Loc.TestAccess -import net.liftweb.sitemap.LocPath.stringToLocPath -import net.liftweb.sitemap.Menu class BrandingPluginDef(override val status: PluginStatus) extends DefaultPluginDef { @@ -64,17 +55,4 @@ class BrandingPluginDef(override val status: PluginStatus) extends DefaultPlugin def oneTimeInit: Unit = {} val configFiles: Seq[ConfigResource] = Seq() - - override def pluginMenuEntry: List[(Menu, Option[String])] = { - ( - (Menu("640-brandingManagement", Branding) / - "secure" / "administration" / "brandingManagement" - >> LocGroup("administrationGroup") - >> TestAccess(() => Boot.userIsAllowed("/secure/administration/policyServerManagement", Administration.Read)) - >> Template(() => - ClasspathTemplates("template" :: "brandingManagement" :: Nil) openOr
Template not found
- )).toMenu, - Some(MenuUtils.utilitiesMenu) - ) :: Nil - } } diff --git a/branding/src/main/scala/com/normation/plugins/branding/snippet/BrandingResources.scala b/branding/src/main/scala/com/normation/plugins/branding/snippet/BrandingResources.scala index 2fcc0e5e8..d373965ad 100644 --- a/branding/src/main/scala/com/normation/plugins/branding/snippet/BrandingResources.scala +++ b/branding/src/main/scala/com/normation/plugins/branding/snippet/BrandingResources.scala @@ -37,18 +37,23 @@ package com.normation.plugins.branding.snippet -import net.liftweb.http.DispatchSnippet +import com.normation.plugins.PluginExtensionPoint +import com.normation.plugins.PluginStatus +import com.normation.rudder.web.ChooseTemplate +import com.normation.rudder.web.snippet.administration.Settings import net.liftweb.http.LiftRules +import scala.reflect.ClassTag import scala.xml.NodeSeq -class BrandingResources extends DispatchSnippet { +class BrandingResources(val status: PluginStatus)(implicit val ttag: ClassTag[Settings]) extends PluginExtensionPoint[Settings] { private[this] def link(s: String) = s"/${LiftRules.resourceServerPath}/branding/${s}" - override def dispatch = { - case "css" => - (_: NodeSeq) => - case "js" => - (_: NodeSeq) => - } + private def template: NodeSeq = ChooseTemplate("template" :: "brandingManagement" :: Nil, "component-body") + + override def pluginCompose(snippet: Settings): Map[String, NodeSeq => NodeSeq] = Map( + "body" -> Settings.addTab("brandingTab", "Branding configuration", template), + "css" -> ((_: NodeSeq) => ), + "js" -> ((_: NodeSeq) => ) + ) } diff --git a/branding/src/main/scala/com/normation/plugins/branding/snippet/CommonBranding.scala b/branding/src/main/scala/com/normation/plugins/branding/snippet/CommonBranding.scala index 0d4ea5f59..50e803aff 100644 --- a/branding/src/main/scala/com/normation/plugins/branding/snippet/CommonBranding.scala +++ b/branding/src/main/scala/com/normation/plugins/branding/snippet/CommonBranding.scala @@ -78,7 +78,7 @@ class CommonBranding(val status: PluginStatus)(implicit val ttag: ClassTag[Commo
case _ => NodeSeq.Empty } - var (customWideLogo, customSmallLogo, rudderLogo) = data match { + val (customWideLogo, customSmallLogo, rudderLogo) = data match { case Right(d) => ( d.wideLogo.commonWideLogo, @@ -87,12 +87,11 @@ class CommonBranding(val status: PluginStatus)(implicit val ttag: ClassTag[Commo { if (d.wideLogo.enable && d.wideLogo.data.isDefined) - Rudder + Rudder else NodeSeq.Empty - } - { + }{ if (d.smallLogo.enable && d.smallLogo.data.isDefined) - Rudder + Rudder else NodeSeq.Empty } @@ -104,7 +103,7 @@ class CommonBranding(val status: PluginStatus)(implicit val ttag: ClassTag[Commo case _ => ( Rudder, - Rudder, + Rudder, NodeSeq.Empty ) } diff --git a/branding/src/main/scala/com/normation/plugins/branding/snippet/LoginBranding.scala b/branding/src/main/scala/com/normation/plugins/branding/snippet/LoginBranding.scala index 5c9ea6a33..f7b3a2243 100644 --- a/branding/src/main/scala/com/normation/plugins/branding/snippet/LoginBranding.scala +++ b/branding/src/main/scala/com/normation/plugins/branding/snippet/LoginBranding.scala @@ -90,7 +90,8 @@ class LoginBranding(val status: PluginStatus, version: PluginVersion)(implicit v }} ) - case _ => (Rudder, NodeSeq.Empty) + case _ => + (Rudder, NodeSeq.Empty) } val logoContainer = {
diff --git a/change-validation/README.adoc b/change-validation/README.adoc index 1e357de4e..5a74e765f 100644 --- a/change-validation/README.adoc +++ b/change-validation/README.adoc @@ -71,7 +71,7 @@ image::docs/images/States.png[] == Change request management page -All Change requests can be seen on the /secure/plugins/changes/changeRequests page. +All Change requests can be seen on the /secure/configurationManager/changes/changeRequests page. There is a table containing all requests, you can access to each of them by clicking on their id. You can filter change requests by status and only display what you need. @@ -79,7 +79,7 @@ image::docs/images/Management.png[] === Change request detail page -Each Change request is reachable on the /secure/plugins/changes/changeRequest/id. +Each Change request is reachable on the /secure/configurationManager/changes/changeRequest/id. image::docs/images/Details.png[] diff --git a/change-validation/src/main/resources/template/ChangeValidationManagement.html b/change-validation/src/main/resources/template/ChangeValidationManagement.html index 640ee8352..6e9666977 100644 --- a/change-validation/src/main/resources/template/ChangeValidationManagement.html +++ b/change-validation/src/main/resources/template/ChangeValidationManagement.html @@ -1,34 +1,20 @@ - - + + - - - - - - -
-Rudder - Change Validation - - - - - + + + + +
-
-
-

- Change validation -

-
-
+
- +
@@ -320,6 +306,5 @@

Configure groups with change validations

buildScrollSpyNav(); }); - - - + + 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 ba507be63..f2f5273a2 100644 --- a/change-validation/src/main/scala/bootstrap/rudder/plugin/ChangeValidationConf.scala +++ b/change-validation/src/main/scala/bootstrap/rudder/plugin/ChangeValidationConf.scala @@ -71,6 +71,7 @@ import com.normation.plugins.changevalidation.api.SupervisedTargetsApi import com.normation.plugins.changevalidation.api.SupervisedTargetsApiImpl import com.normation.plugins.changevalidation.api.ValidatedUserApi import com.normation.plugins.changevalidation.api.ValidatedUserApiImpl +import com.normation.plugins.changevalidation.extension.ChangeValidationTab import com.normation.rudder.domain.nodes.NodeGroupId import com.normation.rudder.domain.policies.DirectiveUid import com.normation.rudder.domain.policies.RuleUid @@ -332,4 +333,5 @@ object ChangeValidationConf extends RudderPluginModule { } RudderConfig.snippetExtensionRegister.register(new TopBarExtension(pluginStatusService)) + RudderConfig.snippetExtensionRegister.register(new ChangeValidationTab(pluginDef.status)) } diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeValidationPluginDef.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeValidationPluginDef.scala index 5b541455a..cd58eedb4 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeValidationPluginDef.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeValidationPluginDef.scala @@ -37,10 +37,8 @@ package com.normation.plugins.changevalidation -import bootstrap.liftweb.Boot import bootstrap.liftweb.Boot.redirection import bootstrap.liftweb.ConfigResource -import bootstrap.liftweb.MenuUtils import bootstrap.rudder.plugin.ChangeValidationConf import com.normation.plugins.* import com.normation.rudder.AuthorizationType @@ -58,7 +56,6 @@ import net.liftweb.http.RedirectWithState import net.liftweb.http.RewriteRequest import net.liftweb.http.RewriteResponse import net.liftweb.sitemap.Loc.Hidden -import net.liftweb.sitemap.Loc.LocGroup import net.liftweb.sitemap.Loc.Template import net.liftweb.sitemap.Loc.TestAccess import net.liftweb.sitemap.LocPath.stringToLocPath @@ -72,18 +69,18 @@ class ChangeValidationPluginDef(override val status: PluginStatus) extends Defau // URL rewrites LiftRules.statefulRewrite.append { case RewriteRequest( - ParsePath("secure" :: "plugins" :: "changes" :: "changeRequests" :: filter :: Nil, _, _, _), + ParsePath("secure" :: "configurationManager" :: "changes" :: "changeRequests" :: filter :: Nil, _, _, _), GetRequest, _ ) => { - RewriteResponse("secure" :: "plugins" :: "changes" :: "changeRequests" :: Nil, Map("filter" -> filter)) + RewriteResponse("secure" :: "configurationManager" :: "changes" :: "changeRequests" :: Nil, Map("filter" -> filter)) } case RewriteRequest( - ParsePath("secure" :: "plugins" :: "changes" :: "changeRequest" :: crId :: Nil, _, _, _), + ParsePath("secure" :: "configurationManager" :: "changes" :: "changeRequest" :: crId :: Nil, _, _, _), GetRequest, _ ) => - RewriteResponse("secure" :: "plugins" :: "changes" :: "changeRequest" :: Nil, Map("crId" -> crId)) + RewriteResponse("secure" :: "configurationManager" :: "changes" :: "changeRequest" :: Nil, Map("crId" -> crId)) } // init directory to save JSON @@ -107,41 +104,25 @@ class ChangeValidationPluginDef(override val status: PluginStatus) extends Defau CurrentUser.checkRights(AuthorizationType.Deployer.Read) (Menu("changeRequests", Change Requests) / - "secure" / "plugins" / "changes" - >> LocGroup("changeValidation") + "secure" / "configurationManager" / "changes" >> Hidden >> TestAccess(() => { if (status.isEnabled() && canViewPage) Empty else - Full(RedirectWithState("/secure/utilities/eventLogs", redirection)) + Full(RedirectWithState("/secure/index", redirection)) })).submenus( Menu("changeRequestsList", Change requests) / - "secure" / "plugins" / "changes" / "changeRequests" + "secure" / "configurationManager" / "changes" / "changeRequests" >> Hidden >> Template(() => ClasspathTemplates("template" :: "changeRequests" :: Nil) openOr
Template not found
), Menu("changeRequestDetails", Change request) / - "secure" / "plugins" / "changes" / "changeRequest" + "secure" / "configurationManager" / "changes" / "changeRequest" >> Hidden >> Template(() => ClasspathTemplates("template" :: "changeRequest" :: Nil) openOr
Template not found
) ) } - override def pluginMenuEntry: List[(Menu, Option[String])] = { - ( - ( - Menu("770-changeValidationManagement", Change validation) / - "secure" / "plugins" / "changeValidationManagement" - >> LocGroup("pluginsGroup") - >> TestAccess(() => Boot.userIsAllowed("/secure/index", AuthorizationType.Administration.Read)) - >> Template(() => - ClasspathTemplates("template" :: "ChangeValidationManagement" :: Nil) openOr
Template not found
- ) - ).toMenu, - Some(MenuUtils.administrationMenu) - ) :: Nil - } - override def updateSiteMap(menus: List[Menu]): List[Menu] = { super.updateSiteMap(menus) :+ changeRequestValidationMenu } diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/comet/WorkflowInformation.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/comet/WorkflowInformation.scala index 65b508129..a3c7da04d 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/comet/WorkflowInformation.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/comet/WorkflowInformation.scala @@ -134,7 +134,7 @@ class WorkflowInformation extends CometActor with CometListener with Loggable { ws.getItemsInStep(TwoValidationStepsWorkflowServiceImpl.Validation.id) match { case Full(seq) =>
  • - + Pending review @@ -163,7 +163,7 @@ class WorkflowInformation extends CometActor with CometListener with Loggable { ws.getItemsInStep(TwoValidationStepsWorkflowServiceImpl.Deployment.id) match { case Full(seq) =>
  • - + Pending deployment diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/extension/ChangeValidationTab.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/extension/ChangeValidationTab.scala new file mode 100644 index 000000000..4067a11e5 --- /dev/null +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/extension/ChangeValidationTab.scala @@ -0,0 +1,55 @@ +/* + ************************************************************************************* + * Copyright 2024 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.plugins.changevalidation.extension + +import com.normation.plugins.PluginExtensionPoint +import com.normation.plugins.PluginStatus +import com.normation.rudder.web.ChooseTemplate +import com.normation.rudder.web.snippet.administration.Settings +import scala.reflect.ClassTag +import scala.xml.NodeSeq + +class ChangeValidationTab(val status: PluginStatus)(implicit val ttag: ClassTag[Settings]) + extends PluginExtensionPoint[Settings] { + + private val template = ChooseTemplate("template" :: "ChangeValidationManagement" :: Nil, "component-body") + + override def pluginCompose(snippet: Settings): Map[String, NodeSeq => NodeSeq] = Map( + "body" -> Settings.addTab("changeValidationTab", "Change validation", template) + ) +} diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestDetails.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestDetails.scala index 765cfb1af..94120aa9c 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestDetails.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestDetails.scala @@ -136,7 +136,7 @@ class ChangeRequestDetails extends DispatchSnippet with Loggable {
  • ++ Script( JsRaw( - s"""setTimeout("location.href = '${S.contextPath}/secure/plugins/changes/changeRequests';",5000);""" + s"""setTimeout("location.href = '${S.contextPath}/secure/configurationManager/changes/changeRequests';",5000);""" ) // JsRaw ok, const ) case Full(cr) => @@ -269,7 +269,7 @@ class ChangeRequestDetails extends DispatchSnippet with Loggable { case (None, Some(_)) => step case (Some(date), Some(stepDate)) => if (date.isAfter(stepDate)) action else step } - ("#backButton [href]" #> "/secure/plugins/changes/changeRequests" & + ("#backButton [href]" #> "/secure/configurationManager/changes/changeRequests" & "#nameTitle *" #> s"CR #${cr.id}: ${cr.info.name}" & "#CRStatus *" #> workflowService .findStep(cr.id) diff --git a/change-validation/src/test/resources/emails/change-validation-email.conf b/change-validation/src/test/resources/emails/change-validation-email.conf index e3d3839bf..24a6f6a44 100644 --- a/change-validation/src/test/resources/emails/change-validation-email.conf +++ b/change-validation/src/test/resources/emails/change-validation-email.conf @@ -7,20 +7,20 @@ smtp.password="thepass" # The rudder base URL (domain) as seen from people who will receive emails. This parameter is used # to display link to change requests in notification emails. -# If the CR URL is: https://my.rudder.server/rudder/secure/plugins/changes/changeRequest/1 +# If the CR URL is: https://my.rudder.server/rudder/secure/configurationManager/changes/changeRequest/1 # You should use: https://my.rudder.server/rudder [without end slash] rudder.base.url="https://my.rudder.server/rudder" # The rudder base URL (domain) as seen from people who will receive emails. This parameter is used # to display link to change requests in notification emails. -# If the CR URL is: https://my.rudder.server/rudder/secure/plugins/changes/changeRequest/1 +# If the CR URL is: https://my.rudder.server/rudder/secure/configurationManager/changes/changeRequest/1 # You should use: https://my.rudder.server/rudder [without end slash] rudder.base.url="https://my.rudder.server/rudder" # `subject` parameter support templating. -# Please refer to the documentation : https://docs.rudder.io/reference/6.1/plugins/change-validation.html +# Please refer to the documentation : https://docs.rudder.io/reference/latest/plugins/change-validation.html # to know which parameters can be used for templating. # Pending validation diff --git a/change-validation/src/test/scala/com/normation/plugins/changevalidation/TestEmailService.scala b/change-validation/src/test/scala/com/normation/plugins/changevalidation/TestEmailService.scala index 25b00c400..06c9a9063 100644 --- a/change-validation/src/test/scala/com/normation/plugins/changevalidation/TestEmailService.scala +++ b/change-validation/src/test/scala/com/normation/plugins/changevalidation/TestEmailService.scala @@ -150,7 +150,7 @@ class TestEmailService extends Specification with BeforeAfterAll { |
  • Author: No One
  • |
  • Description: this CR is for test
  • | - |
    Click here to review and validate + |Click here to review and validate | |""".stripMargin } diff --git a/datasources/src/main/scala/com/normation/plugins/datasources/DataSourcesPluginDef.scala b/datasources/src/main/scala/com/normation/plugins/datasources/DataSourcesPluginDef.scala index 2c2447905..387b9d1ec 100644 --- a/datasources/src/main/scala/com/normation/plugins/datasources/DataSourcesPluginDef.scala +++ b/datasources/src/main/scala/com/normation/plugins/datasources/DataSourcesPluginDef.scala @@ -48,7 +48,6 @@ import com.normation.rudder.rest.EndpointSchema import com.normation.rudder.rest.lift.LiftApiModuleProvider import com.normation.zio.* import net.liftweb.http.ClasspathTemplates -import net.liftweb.sitemap.Loc.LocGroup import net.liftweb.sitemap.Loc.Template import net.liftweb.sitemap.Loc.TestAccess import net.liftweb.sitemap.LocPath.stringToLocPath @@ -73,7 +72,6 @@ class DataSourcesPluginDef(override val status: PluginStatus) extends DefaultPlu ( (Menu("150-dataSourceManagement", Data sources) / "secure" / "plugins" / "dataSourceManagement" - >> LocGroup("pluginsGroup") >> TestAccess(() => Boot.userIsAllowed("/secure/index", Administration.Read)) >> Template(() => ClasspathTemplates("template" :: "dataSourceManagement" :: Nil) openOr
    Template not found
    diff --git a/node-external-reports/src/main/scala/com/normation/plugins/nodeexternalreports/NodeExternalReportPluginDef.scala b/node-external-reports/src/main/scala/com/normation/plugins/nodeexternalreports/NodeExternalReportPluginDef.scala index 6443f8a4f..0e6d53e9d 100644 --- a/node-external-reports/src/main/scala/com/normation/plugins/nodeexternalreports/NodeExternalReportPluginDef.scala +++ b/node-external-reports/src/main/scala/com/normation/plugins/nodeexternalreports/NodeExternalReportPluginDef.scala @@ -45,7 +45,6 @@ import com.normation.plugins.nodeexternalreports.service.NodeExternalReportApi import net.liftweb.common.Loggable import net.liftweb.http.ClasspathTemplates import net.liftweb.http.LiftRules -import net.liftweb.sitemap.Loc.LocGroup import net.liftweb.sitemap.Loc.Template import net.liftweb.sitemap.LocPath.stringToLocPath import net.liftweb.sitemap.Menu @@ -67,7 +66,6 @@ class NodeExternalReportsPluginDef(api: NodeExternalReportApi, override val stat override def pluginMenuEntry: List[(Menu, Option[String])] = { ( (Menu("160-nodeExternalReportInfo", Node external reports) / "secure" / "plugins" / "nodeexternalreports" >> - LocGroup("pluginsGroup") >> Template(() => { ClasspathTemplates("nodeExternalReports" :: Nil) openOr
    Template not found
    diff --git a/node-external-reports/src/main/scala/com/normation/plugins/nodeexternalreports/extension/CreateNodeDetailsExtension.scala b/node-external-reports/src/main/scala/com/normation/plugins/nodeexternalreports/extension/CreateNodeDetailsExtension.scala index 8cb14b000..e2956ba50 100644 --- a/node-external-reports/src/main/scala/com/normation/plugins/nodeexternalreports/extension/CreateNodeDetailsExtension.scala +++ b/node-external-reports/src/main/scala/com/normation/plugins/nodeexternalreports/extension/CreateNodeDetailsExtension.scala @@ -42,11 +42,9 @@ import com.normation.plugins.nodeexternalreports.service.NodeExternalReport import com.normation.plugins.nodeexternalreports.service.ReadExternalReports import com.normation.rudder.users.CurrentUser import com.normation.rudder.web.components.ShowNodeDetailsFromNode - import net.liftweb.common.* import net.liftweb.util.CssSel import net.liftweb.util.Helpers.* - import scala.reflect.ClassTag import scala.xml.NodeSeq From 651a69b5068fa8dfe515f85bad1e3f96d222f7c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Membr=C3=A9?= Date: Fri, 10 Jan 2025 12:48:42 +0100 Subject: [PATCH 11/65] Prepare next branch of plugin --- main-build.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main-build.conf b/main-build.conf index 78f4b4db3..a7812a470 100644 --- a/main-build.conf +++ b/main-build.conf @@ -14,6 +14,6 @@ # Version of Rudder used to build the plugin. # It defined the API/ABI used and it is important for binary compatibility branch-type=next -rudder-version=8.3.0~alpha1 +rudder-version=9.0.0~alpha1 common-version=2.1.1 private-version=2.1.0 From 9c1eb799b73ad0a9900aa7151d42096c19c3f454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Membr=C3=A9?= Date: Fri, 10 Jan 2025 13:16:45 +0100 Subject: [PATCH 12/65] Switch to branch 9.0 --- Jenkinsfile | 2 +- Jenkinsfile-security | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index e747abaeb..5ee3711ba 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,6 +1,6 @@ def failedBuild = false -def minor_version = "8.3" +def minor_version = "9.0" def version = "${minor_version}" def changeUrl = env.CHANGE_URL def slackResponse = null diff --git a/Jenkinsfile-security b/Jenkinsfile-security index 90ff09b2d..5e636997b 100644 --- a/Jenkinsfile-security +++ b/Jenkinsfile-security @@ -1,5 +1,5 @@ -def version = "8.3" +def version = "9.0" def changeUrl = env.CHANGE_URL def job = "" def errors = [] From 0f3d7c197fc7d3a9085c4615bb4fe377e9cd0442 Mon Sep 17 00:00:00 2001 From: Clark Andrianasolo Date: Thu, 16 Jan 2025 11:20:20 +0100 Subject: [PATCH 13/65] Fixes #26195: Maven shade plugin update and ignore signatures --- plugins-common/pom-template.xml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/plugins-common/pom-template.xml b/plugins-common/pom-template.xml index d5d17a953..fd7d2de44 100644 --- a/plugins-common/pom-template.xml +++ b/plugins-common/pom-template.xml @@ -163,7 +163,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.1.1 + 3.6.0 ${project.artifactId}-${project.version}-jar-with-dependencies @@ -174,6 +174,18 @@ shade + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + From 06cbd84c400e9a9689684262c527aaa3cd5544a5 Mon Sep 17 00:00:00 2001 From: Clark Andrianasolo Date: Thu, 16 Jan 2025 15:01:15 +0100 Subject: [PATCH 14/65] Fixes #26199: Janino dependency was not provided to plugins causing plugin load issue --- plugins-common/pom-template.xml | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/plugins-common/pom-template.xml b/plugins-common/pom-template.xml index fd7d2de44..0d9771bc5 100644 --- a/plugins-common/pom-template.xml +++ b/plugins-common/pom-template.xml @@ -174,18 +174,6 @@ shade - - - - *:* - - META-INF/*.SF - META-INF/*.DSA - META-INF/*.RSA - - - - @@ -334,6 +322,7 @@ org.bouncycastlebcpkix-${bouncycastle-compat}provided${bouncycastle-version} org.bouncycastlebcprov-${bouncycastle-compat}provided${bouncycastle-version} org.bouncycastlebcutil-${bouncycastle-compat}provided${bouncycastle-version} + org.codehaus.janinojaninoprovided${janino-version} org.checkerframeworkchecker-qualprovided org.eclipse.jgitorg.eclipse.jgitprovided${jgit-version} org.graalvm.jsjs-languageprovided${graalvm-version} From bce5845c5e6494d8904983ec2309afa35bd0d174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Membr=C3=A9?= Date: Tue, 21 Jan 2025 11:29:22 +0100 Subject: [PATCH 15/65] Prepare next nightly branch of plugin --- main-build.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main-build.conf b/main-build.conf index 78f4b4db3..00cb85169 100644 --- a/main-build.conf +++ b/main-build.conf @@ -14,6 +14,6 @@ # Version of Rudder used to build the plugin. # It defined the API/ABI used and it is important for binary compatibility branch-type=next -rudder-version=8.3.0~alpha1 +rudder-version=8.3.0~alpha2 common-version=2.1.1 private-version=2.1.0 From 36ffebeb0110f62452a5027672eedcd4cca14a7d Mon Sep 17 00:00:00 2001 From: "Francois @fanf42 Armand" Date: Mon, 13 Jan 2025 10:25:30 +0100 Subject: [PATCH 16/65] Fixes #26168: Add OAuth2 Bearer token with client_credentials flow for Rudder API authentication --- auth-backends/pom-template.xml | 5 + ...icationContext-security-auth-oauth2Api.xml | 42 ++ .../rudder/plugin/AuthBackendsConf.scala | 625 ++++++++++++++---- .../plugins/authbackends/DataTypes.scala | 6 +- .../authbackends/Oauth2Authentication.scala | 530 ++++++++++----- .../jwt/jwt_reverse_role_mapping.properties | 18 + .../test/resources/jwt/jwt_simple.properties | 14 + .../oidc/oidc_reverse_role_mapping.properties | 8 + .../resources/oidc/oidc_simple.properties | 6 + .../resources/oidc/oidc_tenants.properties | 29 + .../authbackends/TestReadOidcConfig.scala | 138 +++- 11 files changed, 1121 insertions(+), 300 deletions(-) create mode 100644 auth-backends/src/main/resources/applicationContext-security-auth-oauth2Api.xml create mode 100644 auth-backends/src/test/resources/jwt/jwt_reverse_role_mapping.properties create mode 100644 auth-backends/src/test/resources/jwt/jwt_simple.properties create mode 100644 auth-backends/src/test/resources/oidc/oidc_tenants.properties diff --git a/auth-backends/pom-template.xml b/auth-backends/pom-template.xml index 49824cc20..359bcde9a 100644 --- a/auth-backends/pom-template.xml +++ b/auth-backends/pom-template.xml @@ -58,6 +58,11 @@ spring-security-oauth2-client ${spring-security-version} + + org.springframework.security + spring-security-oauth2-resource-server + ${spring-security-version} + org.springframework.security spring-security-oauth2-jose diff --git a/auth-backends/src/main/resources/applicationContext-security-auth-oauth2Api.xml b/auth-backends/src/main/resources/applicationContext-security-auth-oauth2Api.xml new file mode 100644 index 000000000..318885100 --- /dev/null +++ b/auth-backends/src/main/resources/applicationContext-security-auth-oauth2Api.xml @@ -0,0 +1,42 @@ + + + + + + + + + + diff --git a/auth-backends/src/main/scala/bootstrap/rudder/plugin/AuthBackendsConf.scala b/auth-backends/src/main/scala/bootstrap/rudder/plugin/AuthBackendsConf.scala index 1c779a99a..9c239d707 100644 --- a/auth-backends/src/main/scala/bootstrap/rudder/plugin/AuthBackendsConf.scala +++ b/auth-backends/src/main/scala/bootstrap/rudder/plugin/AuthBackendsConf.scala @@ -50,14 +50,23 @@ import com.normation.plugins.authbackends.AuthBackendsPluginDef import com.normation.plugins.authbackends.AuthBackendsRepositoryImpl import com.normation.plugins.authbackends.CheckRudderPluginEnableImpl import com.normation.plugins.authbackends.LoginFormRendering +import com.normation.plugins.authbackends.ProvidedList import com.normation.plugins.authbackends.RudderClientRegistration +import com.normation.plugins.authbackends.RudderJwtRegistration +import com.normation.plugins.authbackends.RudderOAuth2Registration +import com.normation.plugins.authbackends.RudderPropertyBasedJwtRegistrationDefinition import com.normation.plugins.authbackends.RudderPropertyBasedOAuth2RegistrationDefinition import com.normation.plugins.authbackends.api.AuthBackendsApiImpl import com.normation.plugins.authbackends.snippet.Oauth2LoginBanner import com.normation.rudder.Role import com.normation.rudder.RudderRoles import com.normation.rudder.api.AclPath +import com.normation.rudder.api.ApiAccount +import com.normation.rudder.api.ApiAccountId +import com.normation.rudder.api.ApiAccountKind +import com.normation.rudder.api.ApiAccountName import com.normation.rudder.api.ApiAuthorization +import com.normation.rudder.api.ApiToken import com.normation.rudder.domain.eventlog.RudderEventActor import com.normation.rudder.domain.logger.ApplicationLoggerPure import com.normation.rudder.domain.logger.PluginLogger @@ -79,11 +88,14 @@ import org.springframework.core.ParameterizedTypeReference import org.springframework.core.convert.converter.Converter import org.springframework.http.RequestEntity import org.springframework.http.ResponseEntity +import org.springframework.security.authentication.AbstractAuthenticationToken import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.AuthenticationProvider import org.springframework.security.core.Authentication import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper +import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService @@ -111,6 +123,15 @@ import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority import org.springframework.security.oauth2.core.user.DefaultOAuth2User import org.springframework.security.oauth2.core.user.OAuth2User import org.springframework.security.oauth2.core.user.OAuth2UserAuthority +import org.springframework.security.oauth2.jwt.Jwt +import org.springframework.security.oauth2.jwt.JwtDecoder +import org.springframework.security.oauth2.jwt.JwtException +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder +import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken +import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter import org.springframework.security.web.DefaultSecurityFilterChain import org.springframework.security.web.authentication.AuthenticationFailureHandler import org.springframework.security.web.authentication.AuthenticationSuccessHandler @@ -121,6 +142,7 @@ import org.springframework.web.client.RestOperations import org.springframework.web.client.RestTemplate import org.springframework.web.client.UnknownContentTypeException import scala.jdk.CollectionConverters.* +import zio.ZIO import zio.syntax.* /* @@ -145,7 +167,9 @@ object AuthBackendsConf extends RudderPluginModule { } } - val oauthBackendNames = Set("oauth2", "oidc") + private val oauthBackendNames = Set(RudderOAuth2UserService.PROTOCOL_ID, RudderOidcUserService.PROTOCOL_ID) + val jwtBackendNames = Set(RudderJwtAuthenticationProvider.PROTOCOL_ID) + RudderConfig.authenticationProviders.addProvider(authBackendsProvider) RudderConfig.authenticationProviders.addProvider(new AuthBackendsProvider() { override def authenticationBackends: Set[String] = oauthBackendNames @@ -153,17 +177,27 @@ object AuthBackendsConf extends RudderPluginModule { s"Oauth2 and OpenID Connect authentication backends provider: '${authenticationBackends.mkString("','")}" override def allowedToUseBackend(name: String): Boolean = pluginStatusService.isEnabled() }) + RudderConfig.authenticationProviders.addProvider(new AuthBackendsProvider() { + override def authenticationBackends: Set[String] = jwtBackendNames + override def name: String = + s"Oauth2 and OpenID Connect authentication backends provider for Bearer token in REST API: '${authenticationBackends.mkString("','")}" + override def allowedToUseBackend(name: String): Boolean = pluginStatusService.isEnabled() + }) - lazy val isOauthConfiguredByUser = { - // We need to know if we have to initialize oauth/oicd specific code and snippet. + lazy val (isOauthConfiguredByUser: Boolean, isJwtConfiguredByUser: Boolean) = { + // We need to know if we have to initialize oauth/oidc specific code and snippet. // For that, we need to look in config file directly, because initialisation is complicated and we have no way to // know what part of auth is initialized before what other. It duplicates parsing, but it seems to be the price // of having plugins & spring. We let the full init be done in rudder itself. val configuredAuthProviders = AuthenticationMethods.getForConfig(RudderProperties.config).map(_.name) - configuredAuthProviders.find(a => oauthBackendNames.contains(a)).isDefined + ( + configuredAuthProviders.exists(a => oauthBackendNames.contains(a)), + configuredAuthProviders.exists(a => jwtBackendNames.contains(a)) + ) } lazy val oauth2registrations = RudderPropertyBasedOAuth2RegistrationDefinition.make().runNow + lazy val jwtRegistration = RudderPropertyBasedJwtRegistrationDefinition.make().runNow override lazy val pluginDef: AuthBackendsPluginDef = new AuthBackendsPluginDef(AuthBackendsConf.pluginStatusService) @@ -257,10 +291,12 @@ class AuthBackendsSpringConfiguration extends ApplicationContextAware { RudderConfig.authenticationProviders.addSpringAuthenticationProvider( RudderOAuth2UserService.PROTOCOL_ID, + // here, we can't use applicationContext.getBean without circular reference. It will be put in Spring cache. oauth2AuthenticationProvider(rudderUserService, registrationRepository, userRepository, roleApiMapping) ) RudderConfig.authenticationProviders.addSpringAuthenticationProvider( RudderOidcUserService.PROTOCOL_ID, + // here, we can't use applicationContext.getBean without circular reference. It will be put in Spring cache. oidcAuthenticationProvider(rudderUserService, registrationRepository, userRepository, roleApiMapping) ) val manager = @@ -274,6 +310,26 @@ class AuthBackendsSpringConfiguration extends ApplicationContextAware { applicationContext.getAutowireCapableBeanFactory.configureBean(newSecurityChain, "mainHttpSecurityFilters") } + + // Adding API authentication protected with OAuth2 thanks to a JWT "bearer token" + if (AuthBackendsConf.isJwtConfiguredByUser) { + val config = applicationContext.getBean("jwtRegistrationRepository", classOf[Option[RudderJwtRegistration]]) + RudderConfig.authenticationProviders.addSpringAuthenticationProvider( + RudderJwtAuthenticationProvider.PROTOCOL_ID, + // here, we can't use applicationContext.getBean without circular reference. It will be put in Spring cache. + oauth2ApiAuthenticationProvider(config) + ) + + val http = applicationContext.getBean("publicApiSecurityFilter", classOf[DefaultSecurityFilterChain]) + val filters = http.getFilters + val manager = + applicationContext.getBean("org.springframework.security.authenticationManager", classOf[AuthenticationManager]) + filters.add(3, new BearerTokenAuthenticationFilter(manager)) + + val newSecurityChain = new DefaultSecurityFilterChain(http.getRequestMatcher, filters) + + applicationContext.getAutowireCapableBeanFactory.configureBean(newSecurityChain, "publicApiSecurityFilter") + } } @Bean def userRepository: UserRepository = RudderConfig.userRepository @@ -285,7 +341,7 @@ class AuthBackendsSpringConfiguration extends ApplicationContextAware { /** * We read configuration for OIDC providers in rudder config files. - * The format is defined in + * The format is defined in documentation and parsed in RudderPropertyBasedOAuth2RegistrationDefinition */ @Bean def clientRegistrationRepository: RudderClientRegistrationRepository = { val registrations = ( @@ -295,17 +351,48 @@ class AuthBackendsSpringConfiguration extends ApplicationContextAware { } yield { r.toMap } + ).catchAll { err => + (if (AuthBackendsConf.isOauthConfiguredByUser) { + AuthBackendsLoggerPure.error(err.fullMsg) + } else { + AuthBackendsLoggerPure.debug(err.fullMsg) + }) *> Map.empty[String, RudderClientRegistration].succeed + }.runNow + + new RudderClientRegistrationRepository(registrations) + } + + /** + * We also need to read configuration for JWT providers in rudder config files. + * The format is defined in documentation and parsed in RudderPropertyBasedJwtRegistrationDefinition + * + * For now, we are able to manage ONLY ONE registration, because SpringSecurity token decoder + * doesn't have any logic to handle more than one JWK url. + */ + @Bean def jwtRegistrationRepository: Option[RudderJwtRegistration] = { + ( + for { + _ <- AuthBackendsConf.jwtRegistration.updateRegistration(RudderProperties.config) + r <- AuthBackendsConf.jwtRegistration.registrations.get + _ <- r match { + case Nil | _ :: Nil => ZIO.unit + case h :: tail => + AuthBackendsLoggerPure.warn( + s"Warning! Rudder JWT only support one provider at a time. Only '${h._1}' will be use, '${tail.map(_._1).mkString("','")}' will be ignored." + ) + } + } yield { + r.headOption.map(_._2) + } ).foldZIO( err => - (if (AuthBackendsConf.isOauthConfiguredByUser) { + (if (AuthBackendsConf.isJwtConfiguredByUser) { AuthBackendsLoggerPure.error(err.fullMsg) } else { AuthBackendsLoggerPure.debug(err.fullMsg) - }) *> Map.empty[String, RudderClientRegistration].succeed, + }) *> None.succeed, ok => ok.succeed ).runNow - - new RudderClientRegistrationRepository(registrations) } /** @@ -375,7 +462,7 @@ class AuthBackendsSpringConfiguration extends ApplicationContextAware { registrationRepository: RudderClientRegistrationRepository, userRepository: UserRepository, roleApiMapping: RoleApiMapping - ) = { + ): OAuth2LoginAuthenticationProvider = { val x = new OAuth2LoginAuthenticationProvider( rudderAuthorizationCodeTokenResponseClient(), oauth2UserService(rudderUserDetailsService, registrationRepository, userRepository, roleApiMapping) @@ -398,20 +485,63 @@ class AuthBackendsSpringConfiguration extends ApplicationContextAware { x.setAuthoritiesMapper(userAuthoritiesMapper) x } -} -// a couple of dedicated user details that have the needed information for the SSO part + // OAuth2 for API with JWT Bearer token + @Bean def rudderJwtTokenConverter: RudderJwtAuthenticationConverter = new RudderJwtAuthenticationConverter( + jwtRegistrationRepository, + RudderConfig.roleApiMapping + ) -class RudderClientRegistrationRepository(val registrations: Map[String, RudderClientRegistration]) - extends ClientRegistrationRepository { - override def findByRegistrationId(registrationId: String): ClientRegistration = { - registrations.get(registrationId) match { - case None => null - case Some(x) => x.registration + @Bean def oauth2ApiAuthenticationProvider( + jwtRegistrationRepository: Option[RudderJwtRegistration] + ): RudderJwtAuthenticationProvider = { + val decoder = jwtRegistrationRepository match { + case Some(reg) => NimbusJwtDecoder.withJwkSetUri(reg.jwkSetUri).build + // build a noop decoder + case None => + new JwtDecoder { + override def decode(token: String): Jwt = { + throw new JwtException("Error: no valid JWT registration is configured in rudder") + } + } } + + new RudderJwtAuthenticationProvider(decoder, rudderJwtTokenConverter) } + +} + +// a couple of dedicated user details that have the needed information for the SSO part + +//////////////// RudderUserDetails from OAuth2/OIDC login //////////////// + +/* + * The `RudderDetails` class that is instantiated from an OAuth2 `client_credentials` login (ie when a user, generally + * a service, asked the IdP for a `Bearer` token and then used that token to access Rudder APIs) + * We can't directly inherit `JwtAuthenticationToken` and `RudderUserDetail` because none is a trait, only classes. + */ +case class RudderOAuth2Token(jwt: JwtAuthenticationToken, rudderUserDetail: RudderUserDetail) + extends JwtAuthenticationToken(jwt.getToken, rudderUserDetail.getAuthorities) with UserDetails { + + this.setDetails(jwt.getDetails) + this.setAuthenticated(jwt.isAuthenticated) + + // this is important, it's the way to identify a RudderUser + override def getPrincipal: AnyRef = rudderUserDetail + + override def getPassword: String = rudderUserDetail.getPassword + override def getUsername: String = rudderUserDetail.getUsername + + override def isAccountNonExpired: Boolean = rudderUserDetail.isAccountNonExpired + override def isAccountNonLocked: Boolean = rudderUserDetail.isAccountNonLocked + override def isCredentialsNonExpired: Boolean = rudderUserDetail.isCredentialsNonExpired + override def isEnabled: Boolean = rudderUserDetail.isEnabled } +/* + * The `RudderDetails` class that is instantiated from an OIDC `authorization_code` login (ie when a user actually + * logged on the IdP) + */ final class RudderOidcDetails(oidc: OidcUser, rudder: RudderUserDetail) extends RudderUserDetail(rudder.account, rudder.status, rudder.roles, rudder.apiAuthz, rudder.nodePerms) with OidcUser { override def getClaims: util.Map[String, AnyRef] = oidc.getClaims @@ -421,12 +551,28 @@ final class RudderOidcDetails(oidc: OidcUser, rudder: RudderUserDetail) override def getName: String = oidc.getName } +/* + * The `RudderDetails` class that is instantiated from an OIDC `authorization_code` login (ie when a user actually + * logged on the IdP) + */ final class RudderOauth2Details(oauth2: OAuth2User, rudder: RudderUserDetail) extends RudderUserDetail(rudder.account, rudder.status, rudder.roles, rudder.apiAuthz, rudder.nodePerms) with OAuth2User { override def getAttributes: util.Map[String, AnyRef] = oauth2.getAttributes override def getName: String = oauth2.getName } +//////////////// User OAuth2/OIDC authentication - `authorization_code` workflows //////////////// + +class RudderClientRegistrationRepository(val registrations: Map[String, RudderClientRegistration]) + extends ClientRegistrationRepository { + override def findByRegistrationId(registrationId: String): ClientRegistration = { + registrations.get(registrationId) match { + case None => null + case Some(x) => x.registration + } + } +} + /* * We need to reimplement these methods because we need to correctly manage errors without having horrible * stack traces in logs. Classes below are reimplementation or extension of the corresponding Spring classes @@ -481,6 +627,212 @@ class RudderDefaultOAuth2AuthorizationRequestResolver( } +object RudderTokenMapping { + + /* + * This is the way to get the list of defined role given what an OAuth2 token gives. + * - we have a notion of "default roles" which are the one the user or the token have by default, without + * token info + * - we can map roles to rudder ones so that IdP role names and Rudder ones are independent + * - we can restrict the available roles to the ones mapped so that a Rogue IdP can't use Rudder internal + * roles names (and chaos ensues if those internal name change) + */ + def getRoles( + reg: RudderOAuth2Registration, + principal: String, // user name or token id + protocolName: String, // oauth2Api, oauth2, oidc + default: Set[Role] + )( + getTokenRoles: String => Option[Set[String]] + ): Set[Role] = { + val roles = if (reg.roles.enabled) { + val filteredRoles = getProvidedList(reg.roles, principal)(getTokenRoles) + + AuthBackendsLogger.trace( + s"IdP configuration has registered role mapping: [${reg.roles.mapping.toList.sorted.map(x => s"${x._1 -> x._2}").mkString("; ")}]" + ) + val mappedRoles: Set[Role] = filteredRoles.flatMap { r => + val role = reg.roles.mapping.get(r) match { + // if the role is not in the mapping, use the provided name as is. + case None => RudderRoles.findRoleByName(r) + case Some(m) => + AuthBackendsLogger.debug( + s"Principal '${principal}': mapping IdP provided role '${r}' to Rudder role '${m}' " + ) + RudderRoles + .findRoleByName(m) + .map(_.map(x => Role.Alias(x, r, s"Alias from ${reg.registrationId} IdP"))) + } + role.runNow.orElse { + AuthBackendsLogger.debug( + s"Role '${r}' does not match any Rudder role, ignoring it for user ${principal}" + ) + None + } + } + + if (mappedRoles.nonEmpty) { + ApplicationLoggerPure.Auth.logEffect.info( + s"Principal '${principal}' role list extended with ${protocolName} provided roles: [${Role + .toDisplayNames(mappedRoles) + .mkString(", ")}] (override: ${reg.roles.overrides})" + ) + } else { + AuthBackendsLogger.debug( + s"No roles provided by ${protocolName} in attribute: ${reg.roles.attributeName} (or attribute is missing, or user-management plugin is missing)" + ) + } + + val roles = if (reg.roles.overrides) { + // override means: don't use user role configured in rudder-users.xml + mappedRoles + } else { + default ++ mappedRoles + } + AuthBackendsLogger.debug( + s"Principal '${principal}' final list of roles: [${roles.map(_.name).mkString(", ")}]" + ) + roles + + } else { + AuthBackendsLogger.debug(s"${protocolName} configuration is not configured to use token provided roles") + default + } + + roles + } + + /* + * This is the way to get the list of defined tenants given what an OAuth2 token gives. + * - we have a notion of "default tenants" which are the one the user or the token have by default, without + * token info + * - we can map tenants to rudder ones so that IdP tenant names and Rudder ones are independent + * - we can restrict the available tenants to the ones mapped so that a Rogue IdP can't use Rudder internal + * tenants names (and chaos ensues if those internal name change - even if tenants should be public names) + */ + def getTenants( + reg: RudderOAuth2Registration, + principal: String, // user name or token id + protocolName: String, // oauth2Api, oauth2, oidc + default: NodeSecurityContext + )( + getTokenTenants: String => Option[Set[String]] + ): NodeSecurityContext = { + val tenants = if (reg.tenants.enabled) { + val filteredTenants = getProvidedList(reg.tenants, principal)(getTokenTenants) + + AuthBackendsLogger.trace( + s"IdP configuration has registered tenant mapping: [${reg.tenants.mapping.toList.sorted.map(x => s"${x._1 -> x._2}").mkString("; ")}]" + ) + + // for the tenant mapping, we don't check with existing tenants because the real list + // will be checked at each request (ie, the intersection is done for each node access) + val mappedTenants = filteredTenants.toList.map { t => + reg.tenants.mapping.get(t) match { + // if the tenant is not in the mapping, use the provided name as is. + case None => t + case Some(m) => + AuthBackendsLogger.debug( + s"Principal '${principal}': mapping IdP provided role '${t}' to Rudder role '${m}' " + ) + m + } + } + + val parsedTenants = NodeSecurityContext.parseList(Some(mappedTenants)) match { + case Left(err) => + AuthBackendsLogger.debug( + s"Parsing provided tenants for ${protocolName} in attribute: ${reg.tenants.attributeName} for principal ${principal} lead to an error, disabling all tenants: ${err.fullMsg}" + ) + NodeSecurityContext.None + case Right(nsc) => + ApplicationLoggerPure.Auth.logEffect.info( + s"Principal '${principal}' tenant list extended with ${protocolName} provided tenants: '${nsc.value}' (override: ${reg.tenants.overrides})" + ) + nsc + } + + val tenants = if (reg.tenants.overrides) { + // override means: don't use user tenants configured in rudder-users.xml + parsedTenants + } else { + default.plus(parsedTenants) + } + AuthBackendsLogger.debug( + s"Principal '${principal}' final list of tenants: '${tenants.value}'" + ) + tenants + + } else { + AuthBackendsLogger.debug(s"${protocolName} configuration is not configured to use token provided tenants") + default + } + + tenants + } + + /* + * A generic method to get the list of string corresponding to the given `ProvidedList`. + */ + def getProvidedList( + provided: ProvidedList, + principal: String // user name or token id + )( + getTokenList: String => Option[Set[String]] + ): Set[String] = { + val custom = { + try { + getTokenList(provided.attributeName) match { + case Some(l) => + l + case None => + AuthBackendsLogger.warn( + s"Principal '${principal}' returned information does not contain an attribute '${provided.attributeName}' " + + s"which is the one configured for custom ${provided.debugName} provisioning (see " + + s"'rudder.auth.oauth2.provider.$${idpID}.${provided.debugName}.attribute' value). " + + s"Please check that the attribute name is correct and that requested scope provides that attribute." + ) + Set.empty[String] + } + } catch { + case ex: Exception => + AuthBackendsLogger.warn( + s"Unable to get custom ${provided.debugName} for user '${principal}' when looking for attribute '${provided.attributeName}' :${ex.getMessage}'" + ) + Set.empty[String] + } + } + + // check if we have role mapping or restriction + val filteredSet = if (provided.restrictToMapping) { + val f = custom.intersect(provided.mapping.keySet) + AuthBackendsLogger.debug( + s"IdP configuration enforce restriction to mapped ${provided.debugName}, resulting filtered list: [${f.mkString(", ")}]" + ) + f + } else custom + + filteredSet + } + + /* + * Map roles to the corresponding API ACL. + */ + def getApiAuthorization(roleApiMapping: RoleApiMapping, roles: Set[Role]): ApiAuthorization.ACL = { + // we derive api authz from users rights + val acl = roleApiMapping + .getApiAclFromRoles(roles.toSeq) + .groupBy(_.path.parts.head) + .flatMap { + case (_, seq) => + seq.sortBy(_.path)(AclPath.orderingaAclPath).sortBy(_.path.parts.head.value) + } + .toList + + ApiAuthorization.ACL(acl) + } +} + trait RudderUserServerMapping[R <: OAuth2UserRequest, U <: OAuth2User, T <: RudderUserDetail with U] { def registrationRepository: RudderClientRegistrationRepository @@ -540,7 +892,7 @@ trait RudderUserServerMapping[R <: OAuth2UserRequest, U <: OAuth2User, T <: Rudd } def buildUser( - optReg: Option[RudderClientRegistration], + optReg: Option[RudderOAuth2Registration], userRequest: R, user: U, roleApiMapping: RoleApiMapping, @@ -548,114 +900,33 @@ trait RudderUserServerMapping[R <: OAuth2UserRequest, U <: OAuth2User, T <: Rudd userBuilder: (U, RudderUserDetail) => T, tenants: NodeSecurityContext ): T = { - val roles = { - optReg match { - case None => - AuthBackendsLogger.trace( - s"No configuration found for ${protocolName} registration id: ${userRequest.getClientRegistration.getRegistrationId}" - ) - rudder.roles // if no registration, use user roles - case Some(reg) => - if (reg.roles.enabled) { - val custom = { - try { - import scala.jdk.CollectionConverters.* - if (user.getAttributes.containsKey(reg.roles.attributeName)) { - user - .getAttribute[java.util.ArrayList[String]](reg.roles.attributeName) - .asScala - .toSet - } else { - AuthBackendsLogger.warn( - s"User '${rudder.getUsername}' returned information does not contain an attribute '${reg.roles.attributeName}' " + - s"which is the one configured for custom role provisioning (see 'rudder.auth.oauth2.provider.$${idpID}.roles.attribute'" + - s" value). Please check that the attribute name is correct and that requested scope provides that attribute." - ) - Set.empty[String] - } - } catch { - case ex: Exception => - AuthBackendsLogger.warn( - s"Unable to get custom roles for user '${rudder.getUsername}' when looking for attribute '${reg.roles.attributeName}' :${ex.getMessage}'" - ) - Set.empty[String] - } - } - - // check if we have role mapping or restriction - val filteredRoles = if (reg.restrictRoleMapping) { - val f = custom.filter(r => reg.roleMapping.keySet.contains(r)) - AuthBackendsLogger.debug( - s"IdP configuration enforce restriction to mapped role, resulting filtered list: [${f.mkString(", ")}]" - ) - f - } else custom - AuthBackendsLogger.trace( - s"IdP configuration has registered role mapping: [${reg.roleMapping.toList.sorted.map(x => s"${x._1 -> x._2}").mkString("; ")}]" - ) - val mappedRoles: Set[Role] = filteredRoles.flatMap { r => - val role = reg.roleMapping.get(r) match { - // if the role is not in the mapping, use the provided name as is. - case None => RudderRoles.findRoleByName(r) - case Some(m) => - AuthBackendsLogger.debug( - s"Principal '${rudder.getUsername}': mapping IdP provided role '${r}' to Rudder role '${m}' " - ) - RudderRoles - .findRoleByName(m) - .map(_.map(x => Role.Alias(x, r, s"Alias from ${reg.registration.getRegistrationId} IdP"))) - } - role.runNow.orElse { - AuthBackendsLogger.debug( - s"Role '${r}' does not match any Rudder role, ignoring it for user ${rudder.getUsername}" - ) - None - } - } - - if (mappedRoles.nonEmpty) { - ApplicationLoggerPure.Auth.logEffect.info( - s"Principal '${rudder.getUsername}' role list extended with ${protocolName} provided roles: [${Role - .toDisplayNames(mappedRoles) - .mkString(", ")}] (override: ${reg.roles.over})" - ) - } else { - AuthBackendsLogger.debug( - s"No roles provided by ${protocolName} in attribute: ${reg.roles.attributeName} (or attribute is missing, or user-management plugin is missing)" - ) - } - - val roles = if (reg.roles.over) { - // override means: don't use user role configured in rudder-users.xml - mappedRoles - } else { - rudder.roles ++ mappedRoles - } - AuthBackendsLogger.debug( - s"Principal '${rudder.getUsername}' final list of roles: [${roles.map(_.name).mkString(", ")}]" - ) - roles - } else { - AuthBackendsLogger.debug(s"${protocolName} configuration is not configured to use token provided roles") - rudder.roles - } - } + val (roles, nsc) = optReg match { + case None => + AuthBackendsLogger.trace( + s"No configuration found for ${protocolName} registration id: ${userRequest.getClientRegistration.getRegistrationId}" + ) + (rudder.roles, tenants) // if no registration, use defaults + + case Some(reg) => + val getAttr = (attributeName: String) => { + if (user.getAttributes.containsKey(attributeName)) { + import scala.jdk.CollectionConverters.* + Some(user.getAttribute[java.util.ArrayList[String]](attributeName).asScala.toSet) + } else None + } + + val roles = RudderTokenMapping.getRoles(reg, rudder.getUsername, protocolId, rudder.roles)(getAttr) + val nsc = RudderTokenMapping.getTenants(reg, rudder.getUsername, protocolId, tenants)(getAttr) + + (roles, nsc) } - // we derive api authz from users rights - val acls = roleApiMapping - .getApiAclFromRoles(roles.toSeq) - .groupBy(_.path.parts.head) - .flatMap { - case (_, seq) => - seq.sortBy(_.path)(AclPath.orderingaAclPath).sortBy(_.path.parts.head.value) - } - .toList - val apiAuthz = ApiAuthorization.ACL(acls) - val userDetails = rudder.copy(roles = roles, apiAuthz = apiAuthz, nodePerms = tenants) + // we derive api authz from users rights + val apiAuthz = RudderTokenMapping.getApiAuthorization(roleApiMapping, roles) + val userDetails = rudder.copy(roles = roles, apiAuthz = apiAuthz, nodePerms = nsc) AuthBackendsLogger.debug( - s"Principal '${rudder.getUsername}' final roles: [${roles.map(_.name).mkString(", ")}], and API authz: ${apiAuthz.debugString}" + s"Principal '${rudder.getUsername}' final roles: [${roles.map(_.name).mkString(", ")}], and API authz: ${apiAuthz.debugString}, and tenants: ${nsc.value}" ) // we need to update roles in all cases userBuilder(user, userDetails) @@ -862,3 +1133,111 @@ class RudderDefaultOAuth2UserService extends DefaultOAuth2UserService { } } } + +//////////////// REST API OAuth2 authentication - `client_credentials` workflows //////////////// + +object RudderJwtAuthenticationConverter { + // normalized in JWT standard + val CLIENT_ID_CLAIM: String = "cid" +} + +class RudderJwtAuthenticationConverter( + clientRegistrationRepository: Option[RudderJwtRegistration], + roleApiMapping: RoleApiMapping +) extends Converter[Jwt, AbstractAuthenticationToken] { + + import bootstrap.rudder.plugin.RudderJwtAuthenticationProvider.PROTOCOL_ID + private val jwtConverter = new JwtAuthenticationConverter() + + override def convert(jwt: Jwt): AbstractAuthenticationToken = { + val t = jwtConverter.convert(jwt).asInstanceOf[JwtAuthenticationToken] + + // Find the registration for that token. It's done by looking at the client ID it must contain. + // We only have the clientId, so we need to check them all + val clientId = t.getToken.getClaimAsString(RudderJwtAuthenticationConverter.CLIENT_ID_CLAIM) + + if (clientId == null) { // we're in Java-land, these things can happen + throw new InvalidBearerTokenException( + s"A JWT Bearer token was received but it doesn't have a 'cid' claim, so we don't have a client ID and the token is invalid" + ) + } else { + + clientRegistrationRepository match { + case None => + throw new InvalidBearerTokenException( + s"A JWT Bearer token was received but we don't have any registration for the provided clientId: ${clientId}" + ) + case Some(registration) => + // check that audience matches the expected one if the registration enforce it + if (registration.audience.check && !t.getToken.getAudience.asScala.contains(registration.audience.value)) { + throw new InvalidBearerTokenException( + s"Audience is not the expected one for token, client with ID ${clientId} must target audience: ${registration.audience.value}, but got ${t.getToken.getAudience.asScala + .mkString(", ")}" + ) + } + + def getAttr(attributeName: String) = t.getToken.getClaimAsStringList(attributeName) match { + case null => None + case x => + import scala.jdk.CollectionConverters.* + Some(x.asScala.toSet) + } + + val roles = RudderTokenMapping.getRoles(registration, t.getName, PROTOCOL_ID, default = Set())(getAttr) + val nsc = + RudderTokenMapping.getTenants(registration, t.getName, PROTOCOL_ID, default = NodeSecurityContext.None)(getAttr) + val apiAuthz = RudderTokenMapping.getApiAuthorization(roleApiMapping, roles) + + // create RudderUserDetails from token + val details: RudderUserDetail = { + val created = new DateTime(jwt.getIssuedAt.toEpochMilli) + val exp = Some(new DateTime(jwt.getExpiresAt.toEpochMilli)) + + RudderUserDetail( + RudderAccount.Api( + ApiAccount( + ApiAccountId(jwt.getId), + ApiAccountKind.PublicApi(apiAuthz, exp), + ApiAccountName(jwt.getId), + ApiToken(jwt.getTokenValue), + "", + isEnabled = true, // always enabled at that point, since the token is valid + created, + created, + nsc + ) + ), + UserStatus.Active, // always active at the point, since the token is valid + roles, + apiAuthz, + nsc + ) + } + + AuthBackendsLogger.debug( + s"Principal from JWT '${details.getUsername}' final roles: [${roles.map(_.name).mkString(", ")}], and API authz: ${apiAuthz.debugString}, and tenants: ${nsc.value}" + ) + + RudderOAuth2Token(t, details) + } + } + } +} + +object RudderJwtAuthenticationProvider { + + val PROTOCOL_ID: String = "oauth2Api" +} + +class RudderJwtAuthenticationProvider(jwtDecoder: JwtDecoder, converter: RudderJwtAuthenticationConverter) + extends AuthenticationProvider { + val jwtAuthenticationProvider = new JwtAuthenticationProvider(jwtDecoder) + jwtAuthenticationProvider.setJwtAuthenticationConverter(converter) + + override def authenticate(authentication: Authentication): Authentication = { + val a = jwtAuthenticationProvider.authenticate(authentication) + a + } + + override def supports(authentication: Class[?]): Boolean = jwtAuthenticationProvider.supports(authentication) +} diff --git a/auth-backends/src/main/scala/com/normation/plugins/authbackends/DataTypes.scala b/auth-backends/src/main/scala/com/normation/plugins/authbackends/DataTypes.scala index 264a0f51f..214b310f6 100644 --- a/auth-backends/src/main/scala/com/normation/plugins/authbackends/DataTypes.scala +++ b/auth-backends/src/main/scala/com/normation/plugins/authbackends/DataTypes.scala @@ -107,7 +107,11 @@ final case class JsonLdapConfig( ldapFilter: ConfigOption ) -final object JsonSerialization { +final case class BasePath(base: String, id: String) { + def path(key: String) = base + "." + id + "." + key +} + +object JsonSerialization { implicit val configOptionEncoder: JsonEncoder[ConfigOption] = DeriveJsonEncoder.gen[ConfigOption] implicit val jsonAdminConfigEncoder: JsonEncoder[JsonAdminConfig] = DeriveJsonEncoder.gen[JsonAdminConfig] implicit val jsonFileConfigEncoder: JsonEncoder[JsonFileConfig] = DeriveJsonEncoder.gen[JsonFileConfig] diff --git a/auth-backends/src/main/scala/com/normation/plugins/authbackends/Oauth2Authentication.scala b/auth-backends/src/main/scala/com/normation/plugins/authbackends/Oauth2Authentication.scala index 37122d3b4..dd934cf14 100644 --- a/auth-backends/src/main/scala/com/normation/plugins/authbackends/Oauth2Authentication.scala +++ b/auth-backends/src/main/scala/com/normation/plugins/authbackends/Oauth2Authentication.scala @@ -39,6 +39,7 @@ import bootstrap.liftweb.UserLogout import bootstrap.rudder.plugin.BuildLogout import cats.data.NonEmptyList import com.normation.errors.* +import com.normation.plugins.authbackends.RudderRegistrationPropertyCommon.readProviders import com.typesafe.config.Config import org.springframework.security.oauth2.client.registration.ClientRegistration import org.springframework.security.oauth2.core.AuthorizationGrantType @@ -59,30 +60,96 @@ import zio.syntax.* * In our case, the configuration is done in the rudder property configuration file, and * we just parse the different client and register them in an in memory immutable map. * - * The other important aspect is the identifier used to match the user on the oauth service. - * For now, that identifier must be use as login in rudder. + * We then have two broad cases: + * - delegating authentication for *users* to an IdP through an interactive process which is called `authorization_code` + * workflow. + * One of the important aspect of that workflow is the identifier used to match the user between the IdP and Rudder. + * For now, that identifier must be used as Rudder login. + * - delegating authentication for *API tokens* to an IdP through a JWT bearer token created by an IdP and for which we + * check the authenticity through a process which is called `client_credentials` workflow. * */ -final case class OIDCProvidedRole(enabled: Boolean, attributeName: String, over: Boolean) { - override def toString: String = if (enabled) s"enabled, list obtained from attribute: ${attributeName} (override: ${over})" +sealed trait ProvidedList { + def debugName: String + def enabled: Boolean + def attributeName: String + def overrides: Boolean + def restrictToMapping: Boolean + def mapping: Map[String, String] + + override def toString: String = if (enabled) s"enabled, list obtained from attribute: ${attributeName} (override: ${overrides})" else "disabled" } +final case class ProvidedRoles( + enabled: Boolean, + attributeName: String, + overrides: Boolean, + restrictToMapping: Boolean, + mapping: Map[String, String] +) extends ProvidedList { + val debugName = "roles" +} + +final case class ProvidedTenants( + enabled: Boolean, + attributeName: String, + overrides: Boolean, + restrictToMapping: Boolean, + mapping: Map[String, String] +) extends ProvidedList { + val debugName = "tenants" +} + +final case class JwtAudience( + check: Boolean, + value: String +) + +/* + * We have to data structures to model the configuration parameters needed for a workflow: + * - the first, `RudderJWTRegistration` is simple and is only used for API protection with Bearer token. It only needs an + * URL to get a public key for signature checking, and some role mapping + * - the second, `RudderClientRegistration`, is complicated because in it, Rudder takes active part in the authentication + * and act for the users to make them log on IdP and then into Rudder. We need it to have a client ID and secret in + * the IdP, and lots of other parameters. + */ + +// properties common to both JWT and OAuth2/OIDC registration +sealed trait RudderOAuth2Registration { + def registrationId: String + def roles: ProvidedRoles + def tenants: ProvidedTenants +} + /* + * API access with JWT (bearer token) - client_credentials workflow + */ +final case class RudderJwtRegistration( + registrationId: String, + jwkSetUri: String, + audience: JwtAudience, + roles: ProvidedRoles, + tenants: ProvidedTenants +) extends RudderOAuth2Registration // there is nothing secret here, no need to override `toString()`. + +/* + * User login - authorization_code workflow. * Data container class to add our properties to spring ClientRegistration ones * We never want to print the secret in logs, so override it. */ final case class RudderClientRegistration( - registration: ClientRegistration, - logoutUrl: Option[String], - logoutRedirectUrl: Option[String], - infoMsg: String, - roles: OIDCProvidedRole, - provisioning: Boolean, - restrictRoleMapping: Boolean, - roleMapping: Map[String, String] -) { + registration: ClientRegistration, + logoutUrl: Option[String], + logoutRedirectUrl: Option[String], + infoMsg: String, + roles: ProvidedRoles, + provisioning: Boolean, + tenants: ProvidedTenants +) extends RudderOAuth2Registration { + override def registrationId: String = registration.getRegistrationId + override def toString: String = { toDebugStringWithSecret.replaceFirst("""clientSecret='([^']+?)'""", "clientSecret='*****'") } @@ -93,36 +160,39 @@ final case class RudderClientRegistration( } /* - * Client registration definition based on rudder property file: - * - read property `rudder.auth.oauth2.client.registrations` which give a comma-separated - * list of client registration to oauth2 providers - * - for each registration, read the needed properties based on template - * `rudder.auth.oauth2.client.${registration}.${propertyName}` + * We want to have same attribute name for same things in both JWT and OAuth2/OIDC configuration files. */ -object RudderPropertyBasedOAuth2RegistrationDefinition { - - val A_NAME = "name" - val A_CLIENT_ID = "client.id" - val A_CLIENT_SECRET = "client.secret" - val A_CLIENT_REDIRECT = "client.redirect" - val A_AUTH_METHOD = "authMethod" - val A_GRANT_TYPE = "grantType" - val A_INFO_MESSAGE = "ui.infoMessage" - val A_SCOPE = "scope" - val A_URI_AUTH = "uri.auth" - val A_URI_TOKEN = "uri.token" - val A_URI_USER_INFO = "uri.userInfo" - val A_URI_JWK_SET = "uri.jwkSet" - val A_URI_LOGOUT = "uri.logout" - val A_URI_LOGOUT_REDIRECT = "uri.logoutRedirect" - val A_PIVOT_ATTR = "userNameAttributeName" - val A_ROLES_ENABLED = "roles.enabled" - val A_ROLES_ATTRIBUTE = "roles.attribute" - val A_ROLES_OVERRIDE = "roles.override" - val A_ENFORCE_ROLE_MAPPING = "roles.mapping.restricted" - val A_ROLE_MAPPING = "roles.mapping.entitlements" // ie: OIDC role = Rudder role - val A_ROLE_REVERSE_MAPPING = "roles.mapping.reverseEntitlements" // ie: Rudder role = OIDC role (overrides mapping) - val A_PROVISIONING = "enableProvisioning" +object RudderRegistrationPropertyCommon { + val A_NAME = "name" + val A_CLIENT_ID = "client.id" + val A_CLIENT_SECRET = "client.secret" + val A_CLIENT_REDIRECT = "client.redirect" + val A_AUTH_METHOD = "authMethod" + val A_GRANT_TYPE = "grantType" + val A_INFO_MESSAGE = "ui.infoMessage" + val A_SCOPE = "scope" + val A_AUDIENCE_CHECK = "audience.check" + val A_AUDIENCE_VALUE = "audience.value" + val A_TENANTS_ENABLED = "tenants.enabled" + val A_TENANTS_ATTRIBUTE = "tenants.attribute" + val A_TENANTS_OVERRIDE = "tenants.override" + val A_ENFORCE_TENANTS_MAPPING = "tenants.mapping.restricted" + val A_TENANTS_MAPPING = "tenants.mapping.entitlements" // ie: OIDC tenant = Rudder tenant + val A_TENANTS_REVERSE_MAPPING = "tenants.mapping.reverseEntitlements" // ie: Rudder tenants = OIDC tenants (overrides mapping) + val A_URI_AUTH = "uri.auth" + val A_URI_TOKEN = "uri.token" + val A_URI_USER_INFO = "uri.userInfo" + val A_URI_JWK_SET = "uri.jwkSet" + val A_URI_LOGOUT = "uri.logout" + val A_URI_LOGOUT_REDIRECT = "uri.logoutRedirect" + val A_PIVOT_ATTRIBUTE = "userNameAttributeName" + val A_ROLES_ENABLED = "roles.enabled" + val A_ROLES_ATTRIBUTE = "roles.attribute" + val A_ROLES_OVERRIDE = "roles.override" + val A_ENFORCE_ROLES_MAPPING = "roles.mapping.restricted" + val A_ROLES_MAPPING = "roles.mapping.entitlements" // ie: OIDC role = Rudder role + val A_ROLES_REVERSE_MAPPING = "roles.mapping.reverseEntitlements" // ie: Rudder role = OIDC role (overrides mapping) + val A_PROVISIONING = "enableProvisioning" val authMethods = { import ClientAuthenticationMethod.* @@ -134,31 +204,37 @@ object RudderPropertyBasedOAuth2RegistrationDefinition { List(AUTHORIZATION_CODE, REFRESH_TOKEN, CLIENT_CREDENTIALS) // PASSWORD and IMPLICIT are deprecated for security reasons } - val baseProperty = "rudder.auth.oauth2.provider" - val registrationAttributes = Map( - A_NAME -> "human readable name to use in the button 'login with XXXX'", - A_CLIENT_ID -> "id generated in the Oauth2 service provider to identify rudder as a client app", - A_CLIENT_SECRET -> "the corresponding secret key", - A_CLIENT_REDIRECT -> "rudder URL to redirect to once authentication is done on the provider (must be resolvable from user browser)", - A_AUTH_METHOD -> s"authentication method to use (${authMethods.map(_.getValue).mkString(",")})", - A_GRANT_TYPE -> s"authorization grant type to use (${grantTypes.map(_.getValue).mkString(",")}", - A_INFO_MESSAGE -> "message displayed in the login form, for example to tell the user what login he must use", - A_SCOPE -> "data scope to request access to", - A_URI_AUTH -> "provider URL to contact for main authentication (see provider documentation)", - A_URI_TOKEN -> "provider URL to contact for token verification (see provider documentation)", - A_URI_USER_INFO -> "provider URL to contact to get user information (see provider documentation)", - A_URI_JWK_SET -> "provider URL to check signature of JWT token (see provider documentation)", - A_URI_LOGOUT -> "(optional) provider URL to logout and end session (see provider documentation).", - A_URI_LOGOUT_REDIRECT -> "(optional) the redirect URL to provide to the IdP after logout", - A_PIVOT_ATTR -> "the attribute used to find local app user", - A_ROLES_ENABLED -> "enable custom role extension by OIDC", - A_ROLES_ATTRIBUTE -> "the attribute to use for list of custom role name. It's content in token must be a array of strings.", - A_ROLES_OVERRIDE -> "keep user configured roles in rudder-user.xml or override them with the one provided in the token", - A_PROVISIONING -> "allows the automatic creation of users in Rudder in they successfully authenticate with OIDC", - A_ROLE_MAPPING -> s"provide a map of alias `IdP role name` -> `Rudder role name`, where each IdP role name is a sub-key of '${A_ROLE_MAPPING}'", - A_ROLE_REVERSE_MAPPING -> s"provide a map of alias `Rudder role name` -> `IdP role name`, where each IdP role name is a sub-key of '${A_ROLE_MAPPING}', useful when the IdP role name contains '='", - A_ENFORCE_ROLE_MAPPING -> "if true (default), restricts roles available by the IdP to the role defined in mapping entitlement. Else the map provides alias for Rudder internal role names." + A_NAME -> "human readable name to use in the button 'login with XXXX'", + A_CLIENT_ID -> "id generated in the Oauth2 service provider to identify rudder as a client app", + A_CLIENT_SECRET -> "the corresponding secret key", + A_CLIENT_REDIRECT -> "rudder URL to redirect to once authentication is done on the provider (must be resolvable from user browser)", + A_AUTH_METHOD -> s"authentication method to use (${authMethods.map(_.getValue).mkString(",")})", + A_GRANT_TYPE -> s"authorization grant type to use (${grantTypes.map(_.getValue).mkString(",")}", + A_INFO_MESSAGE -> "message displayed in the login form, for example to tell the user what login he must use", + A_SCOPE -> "data scope to request access to", + A_AUDIENCE_CHECK -> "(default 'true') in the case of JWT, does Rudder need to check the 'aud' claim of the token?", + A_AUDIENCE_VALUE -> "(default 'io.rudder.api') in the case of JWT, the audience value that the token must have to be given access to an API", + A_TENANTS_ENABLED -> "(default false) if enable, restrict the authentication to the provided list of tenants", + A_TENANTS_ATTRIBUTE -> "(default '') the name of the attribute containing the list of authorized tenants", + A_TENANTS_OVERRIDE -> "(default false) keep user configured tenants in rudder-user.xml or override them with the one provided in the token", + A_TENANTS_MAPPING -> s"(optional) provides a map of alias `IdP tenant name` -> `Rudder tenant name`, where each IdP tenant name is a sub-key of '${A_TENANTS_MAPPING}'", + A_TENANTS_REVERSE_MAPPING -> s"(optional) provides a map of alias `Rudder tenant name` -> `IdP tenant name`, where each IdP tenant name is a sub-key of '${A_TENANTS_MAPPING}', useful when the IdP tenant name contains '='", + A_ENFORCE_TENANTS_MAPPING -> "(default true) if true, restricts roles available by the IdP to the role defined in mapping entitlement. Else the map provides alias for Rudder internal role names.", + A_URI_AUTH -> "provider URL to contact for main authentication (see provider documentation)", + A_URI_TOKEN -> "provider URL to contact for token verification (see provider documentation)", + A_URI_USER_INFO -> "provider URL to contact to get user information (see provider documentation)", + A_URI_JWK_SET -> "provider URL to check signature of JWT token (see provider documentation)", + A_URI_LOGOUT -> "(optional) provider URL to logout and end session (see provider documentation).", + A_URI_LOGOUT_REDIRECT -> "(optional) the redirect URL to provide to the IdP after logout", + A_PIVOT_ATTRIBUTE -> "the attribute used to find local app user", + A_ROLES_ENABLED -> "(default false) enable custom role extension by OIDC", + A_ROLES_ATTRIBUTE -> "the attribute to use for list of custom role name. It's content in token must be a array of strings.", + A_ROLES_OVERRIDE -> "(default false) keep user configured roles in rudder-user.xml or override them with the one provided in the token", + A_PROVISIONING -> "(default false) allows the automatic creation of users in Rudder in they successfully authenticate with OIDC", + A_ROLES_MAPPING -> s"(optional) provides a map of alias `IdP role name` -> `Rudder role name`, where each IdP role name is a sub-key of '${A_ROLES_MAPPING}'", + A_ROLES_REVERSE_MAPPING -> s"(optional) provides a map of alias `Rudder role name` -> `IdP role name`, where each IdP role name is a sub-key of '${A_ROLES_MAPPING}', useful when the IdP role name contains '='", + A_ENFORCE_ROLES_MAPPING -> "(default true) if true, restricts roles available by the IdP to the role defined in mapping entitlement. Else the map provides alias for Rudder internal role names." ) def parseAuthenticationMethod(method: String): PureResult[ClientAuthenticationMethod] = { @@ -172,7 +248,7 @@ object RudderPropertyBasedOAuth2RegistrationDefinition { case _ => Left( Inconsistency( - s"Requested OAUTh2 authentication methods '${method}' is not recognized, please use one of: ${authMethods.map(_.getValue).mkString("'", "','", "'")}" + s"Requested OAUTH2 authentication methods '${method}' is not recognized, please use one of: ${authMethods.map(_.getValue).mkString("'", "','", "'")}" ) ) } @@ -185,7 +261,7 @@ object RudderPropertyBasedOAuth2RegistrationDefinition { case None => Left( Inconsistency( - s"Requested OAUTh2 authorization grant type '${grant}' is not recognized, please use one of: ${grantTypes.map(_.getValue).mkString("'", "','", "'")}" + s"Requested OAUTH2 authorization grant type '${grant}' is not recognized, please use one of: ${grantTypes.map(_.getValue).mkString("'", "','", "'")}" ) ) case Some(m) => Right(m) @@ -222,97 +298,81 @@ object RudderPropertyBasedOAuth2RegistrationDefinition { } } - def readOneRegistration(id: String, config: Config): IOResult[RudderClientRegistration] = { + // utility method that read key in config and fails with an error message if + // not found. + protected[authbackends] def read(key: String)(implicit base: BasePath, config: Config): IOResult[String] = { + val path = base.path(key) + IOResult.attempt(s"Missing key '${path}' for registration '${base.id}' (${registrationAttributes(key)})")( + config.getString(path) + ) + } - // utility method that read key in config and fails with an error message if - // not found. - def read(key: String): IOResult[String] = { - val path = baseProperty + "." + id + "." + key - IOResult.attempt(s"Missing key '${path}' for OAUTH2 registration '${id}' (${registrationAttributes(key)})")( - config.getString(path) - ) - } + protected[authbackends] def toBool(s: String) = s.toLowerCase match { + case "true" => true + case _ => false + } - def toBool(s: String) = s.toLowerCase match { - case "true" => true - case _ => false - } - def readMap(key: String): IOResult[Map[String, String]] = { - val path = baseProperty + "." + id + "." + key - import scala.jdk.CollectionConverters.* - for { - keySet <- IOResult - .attempt(s"Missing key '${path}' for OAUTH2 registration '${id}' (${registrationAttributes(key)})")( - config.getConfig(path).entrySet().asScala.map(_.getKey()).toList - ) - .catchAll(_ => - List().succeed - ) // in that case, we suppose that the key is just missing so we return a default empty value for mapping - values <- ZIO.foreach(keySet) { key => - val wholeKey = path + "." + key - IOResult.attempt(s"Error when reading role entitlement mapping '${wholeKey}'") { - (key, config.getString(wholeKey)) - } + protected[authbackends] def readMap( + key: String + )(implicit base: BasePath, config: Config): IOResult[Map[String, String]] = { + val path = base.path(key) + import scala.jdk.CollectionConverters.* + for { + keySet <- IOResult + .attempt(s"Missing key '${path}' for OAUTH2 registration '${base.id}' (${registrationAttributes(key)})")( + config.getConfig(path).entrySet().asScala.map(_.getKey()).toList + ) + .catchAll(_ => + List().succeed + ) // in that case, we suppose that the key is just missing so we return a default empty value for mapping + values <- ZIO.foreach(keySet) { key => + val wholeKey = path + "." + key + IOResult.attempt(s"Error when reading role entitlement mapping '${wholeKey}'") { + (key, config.getString(wholeKey)) } - } yield values.toMap + } + } yield values.toMap + } + + protected[authbackends] def readRoles()(implicit base: BasePath, config: Config): IOResult[ProvidedRoles] = { + for { + rolesEnabled <- read(A_ROLES_ENABLED).catchAll(_ => "false".succeed) + rolesAttr <- read(A_ROLES_ATTRIBUTE).catchAll(_ => "".succeed) + rolesOverride <- read(A_ROLES_OVERRIDE).catchAll(_ => "false".succeed) + enforceRoleMapping <- read(A_ENFORCE_ROLES_MAPPING).catchAll(_ => "false".succeed) + mapping <- readMap(A_ROLES_MAPPING) + reverseMapping <- readMap(A_ROLES_REVERSE_MAPPING) + } yield { + ProvidedRoles( + toBool(rolesEnabled), + rolesAttr, + toBool(rolesOverride), + toBool(enforceRoleMapping), + mapping ++ reverseMapping.map { case (a, b) => (b, a) } + ) } + } + protected[authbackends] def readTenants()(implicit base: BasePath, config: Config): IOResult[ProvidedTenants] = { for { - name <- read(A_NAME) - clientId <- read(A_CLIENT_ID) - clientSecret <- read(A_CLIENT_SECRET) - clientRedirect <- read(A_CLIENT_REDIRECT) - authMethod <- read(A_AUTH_METHOD).flatMap(parseAuthenticationMethod(_).toIO) - grantTypes <- read(A_GRANT_TYPE).flatMap(parseAuthorizationGrantType(_).toIO) - infoMessage <- read(A_INFO_MESSAGE) - scopes <- read(A_SCOPE).flatMap(parseScope(_)) - uriAuth <- read(A_URI_AUTH) - uriToken <- read(A_URI_TOKEN) - uriUserInfo <- read(A_URI_USER_INFO) - pivotAttr <- read(A_PIVOT_ATTR) - jwkSetUri <- read(A_URI_JWK_SET) - logoutUri <- read(A_URI_LOGOUT).fold(_ => Option.empty[String], Some(_)) - logoutUriRedirect <- read(A_URI_LOGOUT_REDIRECT).fold(_ => Option.empty[String], Some(_)) - rolesEnabled <- read(A_ROLES_ENABLED).catchAll(_ => "false".succeed) - rolesAttr <- read(A_ROLES_ATTRIBUTE).catchAll(_ => "".succeed) - rolesOverride <- read(A_ROLES_OVERRIDE).catchAll(_ => "false".succeed) - provisioningAllowed <- read(A_PROVISIONING).catchAll(_ => "false".succeed) - enforceRoleMapping <- read(A_ENFORCE_ROLE_MAPPING).catchAll(_ => "false".succeed) - roleMapping <- readMap(A_ROLE_MAPPING) - roleReverseMapping <- readMap(A_ROLE_REVERSE_MAPPING) + tenantsEnabled <- read(A_TENANTS_ENABLED).catchAll(_ => "false".succeed) + tenantsAttr <- read(A_TENANTS_ATTRIBUTE).catchAll(_ => "".succeed) + tenantsOverride <- read(A_TENANTS_OVERRIDE).catchAll(_ => "false".succeed) + enforceRoleMapping <- read(A_ENFORCE_TENANTS_MAPPING).catchAll(_ => "false".succeed) + mapping <- readMap(A_TENANTS_MAPPING) + reverseMapping <- readMap(A_TENANTS_REVERSE_MAPPING) } yield { - RudderClientRegistration( - ClientRegistration - .withRegistrationId(id) - .clientId(clientId) - .clientSecret(clientSecret) - .clientAuthenticationMethod(authMethod) - .authorizationGrantType(grantTypes) - .redirectUri(clientRedirect) - .scope(scopes*) - .authorizationUri(uriAuth) - .tokenUri(uriToken) - .userInfoUri(uriUserInfo) - .userNameAttributeName(pivotAttr) - .clientName(name) - .jwkSetUri(jwkSetUri) - .build(), - logoutUri, - logoutUriRedirect, - infoMessage, - OIDCProvidedRole( - toBool(rolesEnabled), - rolesAttr, - toBool(rolesOverride) - ), - toBool(provisioningAllowed), + ProvidedTenants( + toBool(tenantsEnabled), + tenantsAttr, + toBool(tenantsOverride), toBool(enforceRoleMapping), - roleMapping ++ roleReverseMapping.map { case (a, b) => (b, a) } + mapping ++ reverseMapping.map { case (a, b) => (b, a) } ) } } - def readProviders(config: Config): IOResult[List[String]] = { + def readProviders(config: Config, baseProperty: String): IOResult[List[String]] = { val path = baseProperty + ".registrations" IOResult.attempt( s"Missing property '${path}' which define the comma separated list of provider registration to use for OAUTH2." @@ -320,22 +380,56 @@ object RudderPropertyBasedOAuth2RegistrationDefinition { config.getString(path).split(",").map(_.trim).toList ) } +} + +/* + * Client registration definition based on rudder property file: + * - read property `rudder.auth.{oauth2,jwt}.client.registrations` which give a comma-separated + * list of client registration to oauth2 providers + * - for each registration, read the needed properties based on template + * `rudder.auth.{oauth2,jwt}.client.${registration}.${propertyName}` + */ + +// a base trait for common methods +trait RudderPropertyBasedRegistration[A <: RudderOAuth2Registration] { + + /* + * Name of the authentication kind to use in logs + */ + def registrationLogName: String + /* + * Name of the root path used for the base property. + */ + def baseProperty: String + + /* + * The list of registration for that kind of JWT/OAuth2 authentication + */ + def registrations: Ref[List[(String, A)]] + + /* + * How to decode one authentication + */ + def readOneRegistration(id: String, config: Config): IOResult[A] /* * Read the whole set of providers with their registrations. * We return a list to keep the order provided in the config file. */ - def readAllRegistrations(config: Config): IOResult[List[(String, RudderClientRegistration)]] = { + def readAllRegistrations( + config: Config, + readOneRegistration: (String, Config) => IOResult[A] + ): IOResult[List[(String, A)]] = { for { // we don't want to fail if the list of provider is missing, just log it as a warning - providers <- readProviders(config) + providers <- readProviders(config, baseProperty) // we don't want to fail if one of the registration is not ok, just log it - _ <- AuthBackendsLoggerPure.info(s"List of configured providers for oauth2/OpenIDConnect: ${providers.mkString(", ")}") + _ <- AuthBackendsLoggerPure.info(s"List of configured providers for ${registrationLogName}: ${providers.mkString(", ")}") registrations <- ZIO.foreach(providers) { p => readOneRegistration(p, config).foldZIO( err => AuthBackendsLoggerPure.error( - s"Error when reading OAUTH2 configuration for registration to '${p}' provider: ${err.fullMsg}'" + s"Error when reading ${registrationLogName} configuration for registration to '${p}' provider: ${err.fullMsg}'" ) *> None.succeed, res => { @@ -347,6 +441,76 @@ object RudderPropertyBasedOAuth2RegistrationDefinition { } yield registrations.flatten } + /* + * read information from config and update internal cache + */ + def updateRegistration(config: Config): IOResult[Unit] + +} + +object RudderPropertyBasedJwtRegistrationDefinition { + + private val baseProperty = "rudder.auth.jwt.provider" + private val registrationLogName = "OAuth2 JWT" + + def make(): IOResult[RudderPropertyBasedJwtRegistrationDefinition] = { + for { + ref <- Ref.make(List.empty[(String, RudderJwtRegistration)]) + } yield { + new RudderPropertyBasedJwtRegistrationDefinition(ref) + } + } + +} + +class RudderPropertyBasedJwtRegistrationDefinition(val registrations: Ref[List[(String, RudderJwtRegistration)]]) + extends RudderPropertyBasedRegistration[RudderJwtRegistration] { + + import com.normation.plugins.authbackends.RudderRegistrationPropertyCommon.* + + override def baseProperty: String = RudderPropertyBasedJwtRegistrationDefinition.baseProperty + override def registrationLogName: String = RudderPropertyBasedJwtRegistrationDefinition.registrationLogName + + def updateRegistration(config: Config): IOResult[Unit] = { + for { + newOnes <- readAllRegistrations(config, readOneRegistration) + _ <- registrations.set(newOnes) + } yield () + } + + def readOneRegistration(id: String, config: Config): IOResult[RudderJwtRegistration] = { + implicit val base = BasePath(baseProperty, id) + implicit val c = config + + for { + jwkSetUri <- read(A_URI_JWK_SET) + checkAudience <- read(A_AUDIENCE_CHECK).catchAll(_ => "true".succeed) + audienceValue <- read(A_AUDIENCE_VALUE).catchAll(_ => "io.rudder.api".succeed) + roles <- readRoles() + tenants <- readTenants() + } yield { + RudderJwtRegistration( + id, + jwkSetUri, + JwtAudience(toBool(checkAudience), audienceValue), + roles, + tenants + ) + } + } +} + +/* + * Client registration definition based on rudder property file: + * - read property `rudder.auth.oauth2.client.registrations` which give a comma-separated + * list of client registration to oauth2 providers + * - for each registration, read the needed properties based on template + * `rudder.auth.oauth2.client.${registration}.${propertyName}` + */ +object RudderPropertyBasedOAuth2RegistrationDefinition { + private val baseProperty = "rudder.auth.oauth2.provider" + private val registrationLogName = "OAuth2/OIDC" + def make(): IOResult[RudderPropertyBasedOAuth2RegistrationDefinition] = { for { ref <- Ref.make(List.empty[(String, RudderClientRegistration)]) @@ -354,18 +518,22 @@ object RudderPropertyBasedOAuth2RegistrationDefinition { new RudderPropertyBasedOAuth2RegistrationDefinition(ref) } } - } -class RudderPropertyBasedOAuth2RegistrationDefinition(val registrations: Ref[List[(String, RudderClientRegistration)]]) { - import RudderPropertyBasedOAuth2RegistrationDefinition.* +class RudderPropertyBasedOAuth2RegistrationDefinition(val registrations: Ref[List[(String, RudderClientRegistration)]]) + extends RudderPropertyBasedRegistration[RudderClientRegistration] { + + import com.normation.plugins.authbackends.RudderRegistrationPropertyCommon.* + + override def baseProperty: String = RudderPropertyBasedOAuth2RegistrationDefinition.baseProperty + override def registrationLogName: String = RudderPropertyBasedOAuth2RegistrationDefinition.registrationLogName /* * read information from config and update internal cache */ - def updateRegistration(config: Config): IOResult[Unit] = { + override def updateRegistration(config: Config): IOResult[Unit] = { for { - newOnes <- readAllRegistrations(config) + newOnes <- readAllRegistrations(config, readOneRegistration) _ <- registrations.set(newOnes) // for each oauthRegistration, register a logout action _ <- ZIO.foreach(newOnes) { @@ -383,4 +551,54 @@ class RudderPropertyBasedOAuth2RegistrationDefinition(val registrations: Ref[Lis } yield () } + override def readOneRegistration(id: String, config: Config): IOResult[RudderClientRegistration] = { + implicit val base = BasePath(baseProperty, id) + implicit val c = config + + for { + name <- read(A_NAME) + clientId <- read(A_CLIENT_ID) + clientSecret <- read(A_CLIENT_SECRET) + clientRedirect <- read(A_CLIENT_REDIRECT) + authMethod <- read(A_AUTH_METHOD).flatMap(parseAuthenticationMethod(_).toIO) + grantTypes <- read(A_GRANT_TYPE).flatMap(parseAuthorizationGrantType(_).toIO) + infoMessage <- read(A_INFO_MESSAGE) + scopes <- read(A_SCOPE).flatMap(parseScope) + uriAuth <- read(A_URI_AUTH) + uriToken <- read(A_URI_TOKEN) + uriUserInfo <- read(A_URI_USER_INFO) + pivotAttr <- read(A_PIVOT_ATTRIBUTE) + jwkSetUri <- read(A_URI_JWK_SET) + logoutUri <- read(A_URI_LOGOUT).fold(_ => Option.empty[String], Some(_)) + logoutUriRedirect <- read(A_URI_LOGOUT_REDIRECT).fold(_ => Option.empty[String], Some(_)) + roles <- readRoles() + tenants <- readTenants() + provisioningAllowed <- read(A_PROVISIONING).catchAll(_ => "false".succeed) + } yield { + RudderClientRegistration( + ClientRegistration + .withRegistrationId(id) + .clientId(clientId) + .clientSecret(clientSecret) + .clientAuthenticationMethod(authMethod) + .authorizationGrantType(grantTypes) + .redirectUri(clientRedirect) + .scope(scopes*) + .authorizationUri(uriAuth) + .tokenUri(uriToken) + .userInfoUri(uriUserInfo) + .userNameAttributeName(pivotAttr) + .clientName(name) + .jwkSetUri(jwkSetUri) + .build(), + logoutUri, + logoutUriRedirect, + infoMessage, + roles, + toBool(provisioningAllowed), + tenants + ) + } + } + } diff --git a/auth-backends/src/test/resources/jwt/jwt_reverse_role_mapping.properties b/auth-backends/src/test/resources/jwt/jwt_reverse_role_mapping.properties new file mode 100644 index 000000000..603191b6c --- /dev/null +++ b/auth-backends/src/test/resources/jwt/jwt_reverse_role_mapping.properties @@ -0,0 +1,18 @@ +rudder.auth.jwt.provider.registrations=someidp +rudder.auth.jwt.provider.someidp.name=Some ID +rudder.auth.jwt.provider.someidp.uri.jwkSet="https://someidp/oauth2/v1/keys" +rudder.auth.jwt.provider.someidp.audience=io.rudder.api +rudder.auth.jwt.provider.someidp.roles.attribute=customroles +rudder.auth.jwt.provider.someidp.roles.mapping.enforced=true +rudder.auth.jwt.provider.someidp.roles.mapping.entitlements.rudder_admin=administrator +rudder.auth.jwt.provider.someidp.roles.mapping.entitlements.rudder_readonly=readonly +rudder.auth.jwt.provider.someidp.roles.mapping.reverseEntitlements.readonlyOVERRIDDEN=rudder_readonly +rudder.auth.jwt.provider.someidp.roles.mapping.reverseEntitlements.administrator=CN=AAAA-BBBBB,OU=Groups,OU=_IT,OU=BB-DD,OU=UUU-XXXX-YY,DC=ee,DC=if,DC=ttttt,DC=uuu +rudder.auth.jwt.provider.someidp.tenants.enabled=true +rudder.auth.jwt.provider.someidp.tenants.attribute=customtenants +rudder.auth.jwt.provider.someidp.tenants.override=true +rudder.auth.jwt.provider.someidp.tenants.mapping.enforced=true +rudder.auth.jwt.provider.someidp.tenants.mapping.entitlements.rudder_TA=TA +rudder.auth.jwt.provider.someidp.tenants.mapping.entitlements.rudder_TB=TB +rudder.auth.jwt.provider.someidp.tenants.mapping.reverseEntitlements.TB_OVERRIDDEN=rudder_TB +rudder.auth.jwt.provider.someidp.tenants.mapping.reverseEntitlements.TA=CN=AAAA-BBBBB,OU=Groups,OU=_IT,OU=BB-DD,OU=UUU-XXXX-YY,DC=ee,DC=if,DC=ttttt,DC=uuu diff --git a/auth-backends/src/test/resources/jwt/jwt_simple.properties b/auth-backends/src/test/resources/jwt/jwt_simple.properties new file mode 100644 index 000000000..b93bad1f5 --- /dev/null +++ b/auth-backends/src/test/resources/jwt/jwt_simple.properties @@ -0,0 +1,14 @@ +rudder.auth.jwt.provider.registrations=someidp +rudder.auth.jwt.provider.someidp.name=Some ID +rudder.auth.jwt.provider.someidp.uri.jwkSet="https://someidp/oauth2/v1/keys" +rudder.auth.jwt.provider.someidp.audience=io.rudder.api +rudder.auth.jwt.provider.someidp.roles.attribute=customroles +rudder.auth.jwt.provider.someidp.roles.mapping.enforced=true +rudder.auth.jwt.provider.someidp.roles.mapping.entitlements.rudder_admin=administrator +rudder.auth.jwt.provider.someidp.roles.mapping.entitlements.rudder_readonly=readonly +rudder.auth.jwt.provider.someidp.tenants.enabled=true +rudder.auth.jwt.provider.someidp.tenants.attribute=customtenants +rudder.auth.jwt.provider.someidp.tenants.override=true +rudder.auth.jwt.provider.someidp.tenants.mapping.enforced=true +rudder.auth.jwt.provider.someidp.tenants.mapping.entitlements.rudder_TA=TA +rudder.auth.jwt.provider.someidp.tenants.mapping.entitlements.rudder_TB=TB diff --git a/auth-backends/src/test/resources/oidc/oidc_reverse_role_mapping.properties b/auth-backends/src/test/resources/oidc/oidc_reverse_role_mapping.properties index ac78ea06b..25da1288d 100644 --- a/auth-backends/src/test/resources/oidc/oidc_reverse_role_mapping.properties +++ b/auth-backends/src/test/resources/oidc/oidc_reverse_role_mapping.properties @@ -21,3 +21,11 @@ rudder.auth.oauth2.provider.someidp.roles.mapping.entitlements.rudder_readonly=r rudder.auth.oauth2.provider.someidp.roles.mapping.reverseEntitlements.readonlyOVERRIDDEN=rudder_readonly rudder.auth.oauth2.provider.someidp.roles.mapping.reverseEntitlements.administrator=CN=AAAA-BBBBB,OU=Groups,OU=_IT,OU=BB-DD,OU=UUU-XXXX-YY,DC=ee,DC=if,DC=ttttt,DC=uuu rudder.auth.oauth2.provider.someidp.enableProvisioning=true +rudder.auth.oauth2.provider.someidp.tenants.enabled=true +rudder.auth.oauth2.provider.someidp.tenants.attribute=customtenants +rudder.auth.oauth2.provider.someidp.tenants.override=true +rudder.auth.oauth2.provider.someidp.tenants.mapping.enforced=true +rudder.auth.oauth2.provider.someidp.tenants.mapping.entitlements.rudder_TA=TA +rudder.auth.oauth2.provider.someidp.tenants.mapping.entitlements.rudder_TB=TB +rudder.auth.oauth2.provider.someidp.tenants.mapping.reverseEntitlements.TB_OVERRIDDEN=rudder_TB +rudder.auth.oauth2.provider.someidp.tenants.mapping.reverseEntitlements.TA=CN=AAAA-BBBBB,OU=Groups,OU=_IT,OU=BB-DD,OU=UUU-XXXX-YY,DC=ee,DC=if,DC=ttttt,DC=uuu diff --git a/auth-backends/src/test/resources/oidc/oidc_simple.properties b/auth-backends/src/test/resources/oidc/oidc_simple.properties index 9841a76a0..d5265174d 100644 --- a/auth-backends/src/test/resources/oidc/oidc_simple.properties +++ b/auth-backends/src/test/resources/oidc/oidc_simple.properties @@ -19,3 +19,9 @@ rudder.auth.oauth2.provider.someidp.roles.mapping.enforced=true rudder.auth.oauth2.provider.someidp.roles.mapping.entitlements.rudder_admin=administrator rudder.auth.oauth2.provider.someidp.roles.mapping.entitlements.rudder_readonly=readonly rudder.auth.oauth2.provider.someidp.enableProvisioning=true +rudder.auth.oauth2.provider.someidp.tenants.enabled=true +rudder.auth.oauth2.provider.someidp.tenants.attribute=customtenants +rudder.auth.oauth2.provider.someidp.tenants.override=true +rudder.auth.oauth2.provider.someidp.tenants.mapping.enforced=true +rudder.auth.oauth2.provider.someidp.tenants.mapping.entitlements.rudder_TA=TA +rudder.auth.oauth2.provider.someidp.tenants.mapping.entitlements.rudder_TB=TB diff --git a/auth-backends/src/test/resources/oidc/oidc_tenants.properties b/auth-backends/src/test/resources/oidc/oidc_tenants.properties new file mode 100644 index 000000000..c6ce29e64 --- /dev/null +++ b/auth-backends/src/test/resources/oidc/oidc_tenants.properties @@ -0,0 +1,29 @@ +rudder.auth.oauth2.provider.registrations=someidp +rudder.auth.oauth2.provider.someidp.name=Some ID +rudder.auth.oauth2.provider.someidp.ui.infoMessage="Hey, log in to Some Idp!" +rudder.auth.oauth2.provider.someidp.client.id=xxxClientIdxxx +rudder.auth.oauth2.provider.someidp.client.secret=xxxClientPassxxx +rudder.auth.oauth2.provider.someidp.scope=openid email profile groups +rudder.auth.oauth2.provider.someidp.userNameAttributeName=email +rudder.auth.oauth2.provider.someidp.uri.auth="https://someidp/oauth2/v1/authorize" +rudder.auth.oauth2.provider.someidp.uri.token="https://someidp/oauth2/v1/token" +rudder.auth.oauth2.provider.someidp.uri.userInfo="https://someidp/oauth2/v1/userinfo" +rudder.auth.oauth2.provider.someidp.uri.jwkSet="https://someidp/oauth2/v1/keys" +rudder.auth.oauth2.provider.someidp.client.redirect="{baseUrl}/login/oauth2/code/{registrationId}" +rudder.auth.oauth2.provider.someidp.grantType=authorization_code +rudder.auth.oauth2.provider.someidp.authMethod=basic +rudder.auth.oauth2.provider.someidp.roles.enabled=true +rudder.auth.oauth2.provider.someidp.roles.attribute=customroles +rudder.auth.oauth2.provider.someidp.roles.override=true +rudder.auth.oauth2.provider.someidp.roles.mapping.enforced=true +rudder.auth.oauth2.provider.someidp.roles.mapping.entitlements.rudder_admin=administrator +rudder.auth.oauth2.provider.someidp.roles.mapping.entitlements.rudder_readonly=readonly +rudder.auth.oauth2.provider.someidp.enableProvisioning=true +rudder.auth.oauth2.provider.someidp.tenants.enabled=true +rudder.auth.oauth2.provider.someidp.tenants.attribute=customtenants +rudder.auth.oauth2.provider.someidp.tenants.override=true +rudder.auth.oauth2.provider.someidp.tenants.mapping.enforced=true +rudder.auth.oauth2.provider.someidp.tenants.mapping.entitlements.rudder_TA=TA +rudder.auth.oauth2.provider.someidp.tenants.mapping.entitlements.rudder_TB=TB +rudder.auth.oauth2.provider.someidp.tenants.mapping.entitlements.rudder_all=* +rudder.auth.oauth2.provider.someidp.tenants.mapping.entitlements.rudder_none=- diff --git a/auth-backends/src/test/scala/com/normation/plugins/authbackends/TestReadOidcConfig.scala b/auth-backends/src/test/scala/com/normation/plugins/authbackends/TestReadOidcConfig.scala index e8ddba33c..f2c6849b7 100644 --- a/auth-backends/src/test/scala/com/normation/plugins/authbackends/TestReadOidcConfig.scala +++ b/auth-backends/src/test/scala/com/normation/plugins/authbackends/TestReadOidcConfig.scala @@ -36,11 +36,15 @@ */ package com.normation.plugins.authbackends +import bootstrap.rudder.plugin.RudderTokenMapping +import com.normation.rudder.facts.nodes.NodeSecurityContext +import com.normation.rudder.tenants.TenantId import com.normation.zio.* import com.typesafe.config.ConfigFactory import org.junit.runner.RunWith import org.specs2.mutable.* import org.specs2.runner.JUnitRunner +import zio.Chunk @RunWith(classOf[JUnitRunner]) class TestReadOidcConfig extends Specification { @@ -48,34 +52,128 @@ class TestReadOidcConfig extends Specification { // WARNING: HOCON doesn't behave the same if you read from a file or from a string, so for the test to be relevant, // we need to load from files. - "reading the configuration should works" >> { + "reading an OIDC configuration" should { + val registration = RudderPropertyBasedOAuth2RegistrationDefinition.make().runNow - val config = ConfigFactory.parseResources("oidc/oidc_simple.properties") - val regs = RudderPropertyBasedOAuth2RegistrationDefinition.readAllRegistrations(config).runNow.toMap + "read the correct name" in { - regs.keySet === Set("someidp") - } + val config = ConfigFactory.parseResources("oidc/oidc_simple.properties") + val regs = registration.readAllRegistrations(config, registration.readOneRegistration).runNow.toMap + + regs.keySet === Set("someidp") + } + + "have two entitlements mapping" in { - "we should have two entitlement mapping" >> { + val config = ConfigFactory.parseResources("oidc/oidc_simple.properties") + val regs = registration.readAllRegistrations(config, registration.readOneRegistration).runNow.toMap - val config = ConfigFactory.parseResources("oidc/oidc_simple.properties") - val regs = RudderPropertyBasedOAuth2RegistrationDefinition.readAllRegistrations(config).runNow.toMap + (regs("someidp").roles.mapping === Map( + "rudder_admin" -> "administrator", + "rudder_readonly" -> "readonly" + )) and (regs("someidp").tenants.mapping === Map( + "rudder_TA" -> "TA", + "rudder_TB" -> "TB" + )) + } + + "be able to use complex roles names with the reverse mapping" in { + val config = ConfigFactory.parseResources("oidc/oidc_reverse_role_mapping.properties") + val regs = registration.readAllRegistrations(config, registration.readOneRegistration).runNow.toMap + + (regs("someidp").roles.mapping === Map( + "rudder_admin" -> "administrator", + "rudder_readonly" -> "readonlyOVERRIDDEN", + "CN=AAAA-BBBBB,OU=Groups,OU=_IT,OU=BB-DD,OU=UUU-XXXX-YY,DC=ee,DC=if,DC=ttttt,DC=uuu" -> "administrator" + )) and ( + regs("someidp").tenants.mapping === Map( + "rudder_TA" -> "TA", + "rudder_TB" -> "TB_OVERRIDDEN", + "CN=AAAA-BBBBB,OU=Groups,OU=_IT,OU=BB-DD,OU=UUU-XXXX-YY,DC=ee,DC=if,DC=ttttt,DC=uuu" -> "TA" + ) + ) + } - regs("someidp").roleMapping === Map( - "rudder_admin" -> "administrator", - "rudder_readonly" -> "readonly" - ) } - "we can use complex roles names with the reverse mapping" >> { - val config = ConfigFactory.parseResources("oidc/oidc_reverse_role_mapping.properties") - val regs = RudderPropertyBasedOAuth2RegistrationDefinition.readAllRegistrations(config).runNow.toMap + "tenants mapping" should { + val config = ConfigFactory.parseResources("oidc/oidc_tenants.properties") + val registration = RudderPropertyBasedOAuth2RegistrationDefinition.make().runNow + val regs = registration.readAllRegistrations(config, registration.readOneRegistration).runNow.toMap + + "work for simple tenants" in { + val tokenValues = Set("rudder_TA", "rudder_TB") + val tenants = + RudderTokenMapping.getTenants(regs("someidp"), "user", "jwt", NodeSecurityContext.None)(_ => Some(tokenValues)) + + tenants === NodeSecurityContext.ByTenants(Chunk(TenantId("TA"), TenantId("TB"))) + } + + "work for no tenants" in { + val tokenValues = Set.empty[String] + val tenants = + RudderTokenMapping.getTenants(regs("someidp"), "user", "jwt", NodeSecurityContext.None)(_ => Some(tokenValues)) + + tenants === NodeSecurityContext.None + } + + "work for none" in { + val tokenValues = Set("rudder_none", "rudder_TA") + val tenants = + RudderTokenMapping.getTenants(regs("someidp"), "user", "jwt", NodeSecurityContext.None)(_ => Some(tokenValues)) + + tenants === NodeSecurityContext.None + } - regs("someidp").roleMapping === Map( - "rudder_admin" -> "administrator", - "rudder_readonly" -> "readonlyOVERRIDDEN", - "CN=AAAA-BBBBB,OU=Groups,OU=_IT,OU=BB-DD,OU=UUU-XXXX-YY,DC=ee,DC=if,DC=ttttt,DC=uuu" -> "administrator" - ) + "work for all" in { + val tokenValues = Set("rudder_all", "rudder_TA") + val tenants = + RudderTokenMapping.getTenants(regs("someidp"), "user", "jwt", NodeSecurityContext.None)(_ => Some(tokenValues)) + + tenants === NodeSecurityContext.All + } } + "reading a JWT configuration" should { + val registration = RudderPropertyBasedJwtRegistrationDefinition.make().runNow + + "read the correct name" in { + + val config = ConfigFactory.parseResources("jwt/jwt_simple.properties") + val regs = registration.readAllRegistrations(config, registration.readOneRegistration).runNow.toMap + + regs.keySet === Set("someidp") + } + + "have two entitlements mapping" in { + + val config = ConfigFactory.parseResources("jwt/jwt_simple.properties") + val regs = registration.readAllRegistrations(config, registration.readOneRegistration).runNow.toMap + + (regs("someidp").roles.mapping === Map( + "rudder_admin" -> "administrator", + "rudder_readonly" -> "readonly" + )) and (regs("someidp").tenants.mapping === Map( + "rudder_TA" -> "TA", + "rudder_TB" -> "TB" + )) + } + + "be able to use complex roles names with the reverse mapping" in { + val config = ConfigFactory.parseResources("jwt/jwt_reverse_role_mapping.properties") + val regs = registration.readAllRegistrations(config, registration.readOneRegistration).runNow.toMap + + (regs("someidp").roles.mapping === Map( + "rudder_admin" -> "administrator", + "rudder_readonly" -> "readonlyOVERRIDDEN", + "CN=AAAA-BBBBB,OU=Groups,OU=_IT,OU=BB-DD,OU=UUU-XXXX-YY,DC=ee,DC=if,DC=ttttt,DC=uuu" -> "administrator" + )) and ( + regs("someidp").tenants.mapping === Map( + "rudder_TA" -> "TA", + "rudder_TB" -> "TB_OVERRIDDEN", + "CN=AAAA-BBBBB,OU=Groups,OU=_IT,OU=BB-DD,OU=UUU-XXXX-YY,DC=ee,DC=if,DC=ttttt,DC=uuu" -> "TA" + ) + ) + } + } } From 69df0c2e94dbc9c5c142b60a837dc65858c449ab Mon Sep 17 00:00:00 2001 From: "Francois @fanf42 Armand" Date: Sun, 2 Feb 2025 18:51:43 +0100 Subject: [PATCH 17/65] Fixes #26291: Add OAuth2 Opaque Accesss Bearer token with client_credentials flow for Rudder API authentication --- .../UserTokenApiDefinition.scala | 3 +- .../apiauthorizations/api/UserApiTest.scala | 2 +- ...ionContext-security-auth-oauth2ApiJwt.xml} | 0 ...ext-security-auth-oauth2ApiOpaqueToken.xml | 42 ++ .../rudder/plugin/AuthBackendsConf.scala | 395 ++++++++++++++---- .../authbackends/Oauth2Authentication.scala | 132 +++++- ...ltOpaqueTokenAuthenticationConverter.scala | 51 +++ .../jwt/jwt_reverse_role_mapping.properties | 36 +- .../test/resources/jwt/jwt_simple.properties | 28 +- .../test/resources/opaque/opaque.properties | 5 + .../opaque/opaque_default.properties | 4 + .../authbackends/TestReadOidcConfig.scala | 29 ++ 12 files changed, 587 insertions(+), 140 deletions(-) rename auth-backends/src/main/resources/{applicationContext-security-auth-oauth2Api.xml => applicationContext-security-auth-oauth2ApiJwt.xml} (100%) create mode 100644 auth-backends/src/main/resources/applicationContext-security-auth-oauth2ApiOpaqueToken.xml create mode 100644 auth-backends/src/main/scala/org/springframework/security/oauth2/server/resource/authentication/DefaultOpaqueTokenAuthenticationConverter.scala create mode 100644 auth-backends/src/test/resources/opaque/opaque.properties create mode 100644 auth-backends/src/test/resources/opaque/opaque_default.properties diff --git a/api-authorizations/src/main/scala/com/normation/plugins/apiauthorizations/UserTokenApiDefinition.scala b/api-authorizations/src/main/scala/com/normation/plugins/apiauthorizations/UserTokenApiDefinition.scala index b8d196013..0d677ffc3 100644 --- a/api-authorizations/src/main/scala/com/normation/plugins/apiauthorizations/UserTokenApiDefinition.scala +++ b/api-authorizations/src/main/scala/com/normation/plugins/apiauthorizations/UserTokenApiDefinition.scala @@ -134,7 +134,7 @@ class UserApiImpl( ApiAccountId(authzToken.qc.actor.name), ApiAccountKind.User, ApiAccountName(authzToken.qc.actor.name), - ApiToken(hash), + Some(ApiToken(hash)), s"API token for user '${authzToken.qc.actor.name}'", isEnabled = true, now, @@ -254,6 +254,7 @@ object UserApiImpl { implicit val transformer: Transformer[ApiAccount, RestApiAccount] = Transformer .define[ApiAccount, RestApiAccount] + .withFieldComputed(_.token, a => ClearTextToken(a.token.map(_.value).getOrElse(""))) .withFieldComputed(_.kind, _.kind.kind) .withFieldComputed(_.acl, _.acl) .withFieldComputed(_.expirationDate, _.expirationDate) diff --git a/api-authorizations/src/test/scala/com/normation/plugins/apiauthorizations/api/UserApiTest.scala b/api-authorizations/src/test/scala/com/normation/plugins/apiauthorizations/api/UserApiTest.scala index 4612acb04..b3e06f266 100644 --- a/api-authorizations/src/test/scala/com/normation/plugins/apiauthorizations/api/UserApiTest.scala +++ b/api-authorizations/src/test/scala/com/normation/plugins/apiauthorizations/api/UserApiTest.scala @@ -43,7 +43,7 @@ class UserApiTest extends ZIOSpecDefault { ApiAccountId("user1"), ApiAccountKind.System, // so that we have access to the plugin endpoints ApiAccountName("user1"), - ApiToken("v2:some-hashed-token"), + Some(ApiToken("v2:some-hashed-token")), "number one user", isEnabled = true, creationDate = accountCreationDate, diff --git a/auth-backends/src/main/resources/applicationContext-security-auth-oauth2Api.xml b/auth-backends/src/main/resources/applicationContext-security-auth-oauth2ApiJwt.xml similarity index 100% rename from auth-backends/src/main/resources/applicationContext-security-auth-oauth2Api.xml rename to auth-backends/src/main/resources/applicationContext-security-auth-oauth2ApiJwt.xml diff --git a/auth-backends/src/main/resources/applicationContext-security-auth-oauth2ApiOpaqueToken.xml b/auth-backends/src/main/resources/applicationContext-security-auth-oauth2ApiOpaqueToken.xml new file mode 100644 index 000000000..318885100 --- /dev/null +++ b/auth-backends/src/main/resources/applicationContext-security-auth-oauth2ApiOpaqueToken.xml @@ -0,0 +1,42 @@ + + + + + + + + + + diff --git a/auth-backends/src/main/scala/bootstrap/rudder/plugin/AuthBackendsConf.scala b/auth-backends/src/main/scala/bootstrap/rudder/plugin/AuthBackendsConf.scala index 9c239d707..6601dee97 100644 --- a/auth-backends/src/main/scala/bootstrap/rudder/plugin/AuthBackendsConf.scala +++ b/auth-backends/src/main/scala/bootstrap/rudder/plugin/AuthBackendsConf.scala @@ -44,29 +44,12 @@ import bootstrap.liftweb.RudderInMemoryUserDetailsService import bootstrap.liftweb.RudderProperties import com.normation.errors.IOResult import com.normation.plugins.RudderPluginModule -import com.normation.plugins.authbackends.AuthBackendsLogger -import com.normation.plugins.authbackends.AuthBackendsLoggerPure -import com.normation.plugins.authbackends.AuthBackendsPluginDef -import com.normation.plugins.authbackends.AuthBackendsRepositoryImpl -import com.normation.plugins.authbackends.CheckRudderPluginEnableImpl -import com.normation.plugins.authbackends.LoginFormRendering -import com.normation.plugins.authbackends.ProvidedList -import com.normation.plugins.authbackends.RudderClientRegistration -import com.normation.plugins.authbackends.RudderJwtRegistration -import com.normation.plugins.authbackends.RudderOAuth2Registration -import com.normation.plugins.authbackends.RudderPropertyBasedJwtRegistrationDefinition -import com.normation.plugins.authbackends.RudderPropertyBasedOAuth2RegistrationDefinition +import com.normation.plugins.authbackends.* import com.normation.plugins.authbackends.api.AuthBackendsApiImpl import com.normation.plugins.authbackends.snippet.Oauth2LoginBanner import com.normation.rudder.Role import com.normation.rudder.RudderRoles -import com.normation.rudder.api.AclPath -import com.normation.rudder.api.ApiAccount -import com.normation.rudder.api.ApiAccountId -import com.normation.rudder.api.ApiAccountKind -import com.normation.rudder.api.ApiAccountName -import com.normation.rudder.api.ApiAuthorization -import com.normation.rudder.api.ApiToken +import com.normation.rudder.api.* import com.normation.rudder.domain.eventlog.RudderEventActor import com.normation.rudder.domain.logger.ApplicationLoggerPure import com.normation.rudder.domain.logger.PluginLogger @@ -125,12 +108,16 @@ import org.springframework.security.oauth2.core.user.OAuth2User import org.springframework.security.oauth2.core.user.OAuth2UserAuthority import org.springframework.security.oauth2.jwt.Jwt import org.springframework.security.oauth2.jwt.JwtDecoder -import org.springframework.security.oauth2.jwt.JwtException import org.springframework.security.oauth2.jwt.NimbusJwtDecoder import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException +import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication +import org.springframework.security.oauth2.server.resource.authentication.DefaultOpaqueTokenAuthenticationConverter import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken +import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider +import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter import org.springframework.security.web.DefaultSecurityFilterChain import org.springframework.security.web.authentication.AuthenticationFailureHandler @@ -167,8 +154,11 @@ object AuthBackendsConf extends RudderPluginModule { } } - private val oauthBackendNames = Set(RudderOAuth2UserService.PROTOCOL_ID, RudderOidcUserService.PROTOCOL_ID) - val jwtBackendNames = Set(RudderJwtAuthenticationProvider.PROTOCOL_ID) + private val oauthBackendNames = Set(RudderOAuth2UserService.PROTOCOL_ID, RudderOidcUserService.PROTOCOL_ID) + // Javascript web token + private val jwtBackendNames = Set(RudderJwtAuthenticationProvider.PROTOCOL_ID) + // opaque bearer token + private val opaqueTokenBackendNames = Set(RudderOpaqueTokenAuthenticationProvider.PROTOCOL_ID) RudderConfig.authenticationProviders.addProvider(authBackendsProvider) RudderConfig.authenticationProviders.addProvider(new AuthBackendsProvider() { @@ -180,11 +170,17 @@ object AuthBackendsConf extends RudderPluginModule { RudderConfig.authenticationProviders.addProvider(new AuthBackendsProvider() { override def authenticationBackends: Set[String] = jwtBackendNames override def name: String = - s"Oauth2 and OpenID Connect authentication backends provider for Bearer token in REST API: '${authenticationBackends.mkString("','")}" + s"Oauth2 and OpenID Connect authentication backends provider for JWT Bearer token in REST API: '${authenticationBackends.mkString("','")}" + override def allowedToUseBackend(name: String): Boolean = pluginStatusService.isEnabled() + }) + RudderConfig.authenticationProviders.addProvider(new AuthBackendsProvider() { + override def authenticationBackends: Set[String] = opaqueTokenBackendNames + override def name: String = + s"Oauth2 and OpenID Connect authentication backends provider for Opaque Bearer token in REST API: '${authenticationBackends.mkString("','")}" override def allowedToUseBackend(name: String): Boolean = pluginStatusService.isEnabled() }) - lazy val (isOauthConfiguredByUser: Boolean, isJwtConfiguredByUser: Boolean) = { + lazy val (isOauthConfiguredByUser: Boolean, isJwtConfiguredByUser: Boolean, isOpaqueTokenConfiguredByUser: Boolean) = { // We need to know if we have to initialize oauth/oidc specific code and snippet. // For that, we need to look in config file directly, because initialisation is complicated and we have no way to // know what part of auth is initialized before what other. It duplicates parsing, but it seems to be the price @@ -192,12 +188,14 @@ object AuthBackendsConf extends RudderPluginModule { val configuredAuthProviders = AuthenticationMethods.getForConfig(RudderProperties.config).map(_.name) ( configuredAuthProviders.exists(a => oauthBackendNames.contains(a)), - configuredAuthProviders.exists(a => jwtBackendNames.contains(a)) + configuredAuthProviders.exists(a => jwtBackendNames.contains(a)), + configuredAuthProviders.exists(a => opaqueTokenBackendNames.contains(a)) ) } - lazy val oauth2registrations = RudderPropertyBasedOAuth2RegistrationDefinition.make().runNow - lazy val jwtRegistration = RudderPropertyBasedJwtRegistrationDefinition.make().runNow + lazy val oauth2registrations = RudderPropertyBasedOAuth2RegistrationDefinition.make().runNow + lazy val jwtRegistrations = RudderPropertyBasedJwtRegistrationDefinition.make().runNow + lazy val opaqueTokenRegistrations = RudderPropertyBasedOpaqueTokenRegistrationDefinition.make().runNow override lazy val pluginDef: AuthBackendsPluginDef = new AuthBackendsPluginDef(AuthBackendsConf.pluginStatusService) @@ -313,22 +311,62 @@ class AuthBackendsSpringConfiguration extends ApplicationContextAware { // Adding API authentication protected with OAuth2 thanks to a JWT "bearer token" if (AuthBackendsConf.isJwtConfiguredByUser) { - val config = applicationContext.getBean("jwtRegistrationRepository", classOf[Option[RudderJwtRegistration]]) - RudderConfig.authenticationProviders.addSpringAuthenticationProvider( - RudderJwtAuthenticationProvider.PROTOCOL_ID, - // here, we can't use applicationContext.getBean without circular reference. It will be put in Spring cache. - oauth2ApiAuthenticationProvider(config) - ) + // only add filter if we have one registration + val optConfig = applicationContext.getBean("jwtRegistrationRepository", classOf[Option[RudderJwtRegistration]]) + optConfig match { + + case Some(config) => + RudderConfig.authenticationProviders.addSpringAuthenticationProvider( + RudderJwtAuthenticationProvider.PROTOCOL_ID, + // here, we can't use applicationContext.getBean without circular reference. It will be put in Spring cache. + oauth2ApiJwtAuthenticationProvider(optConfig) + ) - val http = applicationContext.getBean("publicApiSecurityFilter", classOf[DefaultSecurityFilterChain]) - val filters = http.getFilters - val manager = - applicationContext.getBean("org.springframework.security.authenticationManager", classOf[AuthenticationManager]) - filters.add(3, new BearerTokenAuthenticationFilter(manager)) + val http = applicationContext.getBean("publicApiSecurityFilter", classOf[DefaultSecurityFilterChain]) + val filters = http.getFilters + val manager = + applicationContext.getBean("org.springframework.security.authenticationManager", classOf[AuthenticationManager]) + filters.add(3, new BearerTokenAuthenticationFilter(manager)) - val newSecurityChain = new DefaultSecurityFilterChain(http.getRequestMatcher, filters) + val newSecurityChain = new DefaultSecurityFilterChain(http.getRequestMatcher, filters) + + applicationContext.getAutowireCapableBeanFactory.configureBean(newSecurityChain, "publicApiSecurityFilter") + + case None => + AuthBackendsLogger.info( + s"${RudderJwtAuthenticationProvider.PROTOCOL_ID} is configured as an authentication provider but there is no valid registration for it: it will be ignored" + ) + } + } - applicationContext.getAutowireCapableBeanFactory.configureBean(newSecurityChain, "publicApiSecurityFilter") + // Adding API authentication protected with OAuth2 thanks to an "opaque access bearer token" + if (AuthBackendsConf.isOpaqueTokenConfiguredByUser) { + // only add filter if we have one registration + val optConfig = + applicationContext.getBean("opaqueTokenRegistrationRepository", classOf[Option[RudderOpaqueTokenRegistration]]) + optConfig match { + case Some(config) => + RudderConfig.authenticationProviders.addSpringAuthenticationProvider( + RudderJwtAuthenticationProvider.PROTOCOL_ID, + // here, we can't use applicationContext.getBean without circular reference. It will be put in Spring cache. + oauth2ApiOpaqueTokenAuthenticationProvider(optConfig) + ) + + val http = applicationContext.getBean("publicApiSecurityFilter", classOf[DefaultSecurityFilterChain]) + val filters = http.getFilters + val manager = + applicationContext.getBean("org.springframework.security.authenticationManager", classOf[AuthenticationManager]) + filters.add(3, new BearerTokenAuthenticationFilter(manager)) + + val newSecurityChain = new DefaultSecurityFilterChain(http.getRequestMatcher, filters) + + applicationContext.getAutowireCapableBeanFactory.configureBean(newSecurityChain, "publicApiSecurityFilter") + + case None => + AuthBackendsLogger.warn( + s"Warning! ${RudderOpaqueTokenAuthenticationProvider.PROTOCOL_ID} is configured as an authentication provider but there is no valid registration for it: it will be ignored" + ) + } } } @@ -372,8 +410,8 @@ class AuthBackendsSpringConfiguration extends ApplicationContextAware { @Bean def jwtRegistrationRepository: Option[RudderJwtRegistration] = { ( for { - _ <- AuthBackendsConf.jwtRegistration.updateRegistration(RudderProperties.config) - r <- AuthBackendsConf.jwtRegistration.registrations.get + _ <- AuthBackendsConf.jwtRegistrations.updateRegistration(RudderProperties.config) + r <- AuthBackendsConf.jwtRegistrations.registrations.get _ <- r match { case Nil | _ :: Nil => ZIO.unit case h :: tail => @@ -384,15 +422,40 @@ class AuthBackendsSpringConfiguration extends ApplicationContextAware { } yield { r.headOption.map(_._2) } - ).foldZIO( - err => - (if (AuthBackendsConf.isJwtConfiguredByUser) { - AuthBackendsLoggerPure.error(err.fullMsg) - } else { - AuthBackendsLoggerPure.debug(err.fullMsg) - }) *> None.succeed, - ok => ok.succeed - ).runNow + ).catchAll(err => { + (if (AuthBackendsConf.isJwtConfiguredByUser) { + AuthBackendsLoggerPure.error(err.fullMsg) + } else { + AuthBackendsLoggerPure.debug(err.fullMsg) + }) *> None.succeed + }).runNow + } + + /* + * The same then JWT, for opaque token registration + */ + @Bean def opaqueTokenRegistrationRepository: Option[RudderOpaqueTokenRegistration] = { + ( + for { + _ <- AuthBackendsConf.opaqueTokenRegistrations.updateRegistration(RudderProperties.config) + r <- AuthBackendsConf.opaqueTokenRegistrations.registrations.get + _ <- r match { + case Nil | _ :: Nil => ZIO.unit + case h :: tail => + AuthBackendsLoggerPure.warn( + s"Warning! Rudder opaque access bearer tokens only support one provider at a time. Only '${h._1}' will be use, '${tail.map(_._1).mkString("','")}' will be ignored." + ) + } + } yield { + r.headOption.map(_._2) + } + ).catchAll { err => + (if (AuthBackendsConf.isOpaqueTokenConfiguredByUser) { + AuthBackendsLoggerPure.error(err.fullMsg) + } else { + AuthBackendsLoggerPure.debug(err.fullMsg) + }) *> None.succeed + }.runNow } /** @@ -486,27 +549,36 @@ class AuthBackendsSpringConfiguration extends ApplicationContextAware { x } - // OAuth2 for API with JWT Bearer token - @Bean def rudderJwtTokenConverter: RudderJwtAuthenticationConverter = new RudderJwtAuthenticationConverter( - jwtRegistrationRepository, - RudderConfig.roleApiMapping - ) - - @Bean def oauth2ApiAuthenticationProvider( + @Bean def oauth2ApiJwtAuthenticationProvider( jwtRegistrationRepository: Option[RudderJwtRegistration] - ): RudderJwtAuthenticationProvider = { - val decoder = jwtRegistrationRepository match { - case Some(reg) => NimbusJwtDecoder.withJwkSetUri(reg.jwkSetUri).build - // build a noop decoder - case None => - new JwtDecoder { - override def decode(token: String): Jwt = { - throw new JwtException("Error: no valid JWT registration is configured in rudder") - } - } + ): AuthenticationProvider = { + jwtRegistrationRepository match { + case Some(config) => + val decoder = NimbusJwtDecoder.withJwkSetUri(config.jwkSetUri).build + val converter = new RudderJwtAuthenticationConverter( + jwtRegistrationRepository, + RudderConfig.roleApiMapping, + config.pivotAttributeName + ) + new RudderJwtAuthenticationProvider(decoder, converter) + + case None => MissingConfigurationAuthenticationProvider } + } + + @Bean def oauth2ApiOpaqueTokenAuthenticationProvider( + opaqueTokenRegistrationRepository: Option[RudderOpaqueTokenRegistration] + ): AuthenticationProvider = { + opaqueTokenRegistrationRepository match { + case Some(config) => + val introspector = new NimbusOpaqueTokenIntrospector(config.introspectUri, config.clientId, config.clientSecret) + new RudderOpaqueTokenAuthenticationProvider( + introspector, + new RudderOpaqueTokenAuthenticationConverter(RudderConfig.roApiAccountRepository, config.pivotAttributeName) + ) - new RudderJwtAuthenticationProvider(decoder, rudderJwtTokenConverter) + case None => MissingConfigurationAuthenticationProvider + } } } @@ -520,7 +592,7 @@ class AuthBackendsSpringConfiguration extends ApplicationContextAware { * a service, asked the IdP for a `Bearer` token and then used that token to access Rudder APIs) * We can't directly inherit `JwtAuthenticationToken` and `RudderUserDetail` because none is a trait, only classes. */ -case class RudderOAuth2Token(jwt: JwtAuthenticationToken, rudderUserDetail: RudderUserDetail) +case class RudderOAuth2Jwt(jwt: JwtAuthenticationToken, rudderUserDetail: RudderUserDetail) extends JwtAuthenticationToken(jwt.getToken, rudderUserDetail.getAuthorities) with UserDetails { this.setDetails(jwt.getDetails) @@ -538,6 +610,30 @@ case class RudderOAuth2Token(jwt: JwtAuthenticationToken, rudderUserDetail: Rudd override def isEnabled: Boolean = rudderUserDetail.isEnabled } +case class RudderOAuth2OpaqueToken(obt: BearerTokenAuthentication, rudderUserDetail: RudderUserDetail) + extends BearerTokenAuthentication( + obt.getPrincipal.asInstanceOf[OAuth2AuthenticatedPrincipal], + obt.getCredentials.asInstanceOf[OAuth2AccessToken], + rudderUserDetail.getAuthorities + ) with UserDetails { + + this.setDetails(obt.getDetails) + this.setAuthenticated(obt.isAuthenticated) + + // this is important, it's the way to identify a RudderUser + override def getPrincipal: AnyRef = rudderUserDetail + + override def getPassword: String = rudderUserDetail.getPassword + override def getUsername: String = rudderUserDetail.getUsername + + override def isAccountNonExpired: Boolean = rudderUserDetail.isAccountNonExpired + override def isAccountNonLocked: Boolean = rudderUserDetail.isAccountNonLocked + override def isCredentialsNonExpired: Boolean = rudderUserDetail.isCredentialsNonExpired + override def isEnabled: Boolean = rudderUserDetail.isEnabled + + override def getTokenAttributes: util.Map[String, AnyRef] = obt.getTokenAttributes +} + /* * The `RudderDetails` class that is instantiated from an OIDC `authorization_code` login (ie when a user actually * logged on the IdP) @@ -638,7 +734,7 @@ object RudderTokenMapping { * roles names (and chaos ensues if those internal name change) */ def getRoles( - reg: RudderOAuth2Registration, + reg: RudderOAuth2Registration with RegistrationWithRoles, principal: String, // user name or token id protocolName: String, // oauth2Api, oauth2, oidc default: Set[Role] @@ -711,7 +807,7 @@ object RudderTokenMapping { * tenants names (and chaos ensues if those internal name change - even if tenants should be public names) */ def getTenants( - reg: RudderOAuth2Registration, + reg: RudderOAuth2Registration with RegistrationWithRoles, principal: String, // user name or token id protocolName: String, // oauth2Api, oauth2, oidc default: NodeSecurityContext @@ -892,7 +988,7 @@ trait RudderUserServerMapping[R <: OAuth2UserRequest, U <: OAuth2User, T <: Rudd } def buildUser( - optReg: Option[RudderOAuth2Registration], + optReg: Option[RudderOAuth2Registration with RegistrationWithRoles], userRequest: R, user: U, roleApiMapping: RoleApiMapping, @@ -1136,14 +1232,50 @@ class RudderDefaultOAuth2UserService extends DefaultOAuth2UserService { //////////////// REST API OAuth2 authentication - `client_credentials` workflows //////////////// -object RudderJwtAuthenticationConverter { - // normalized in JWT standard - val CLIENT_ID_CLAIM: String = "cid" +/////////////// OAuth2/OIDC - JWT bearer token /////////////// + +/* + * A placeholder AuthenticationProvider to use when we don't have any registration. + * Spring undecidable initialisation patterns force us to do strange things. + */ +object MissingConfigurationAuthenticationProvider extends AuthenticationProvider { + override def authenticate(authentication: Authentication): Authentication = { + throw new OAuth2AuthenticationException( + s"This authentication provider is missing configuration and can't be called for authentication" + ) + } + override def supports(authentication: Class[?]): Boolean = false +} + +/* + * Authentication provider for OAuth2/OIDC for protecting APIs with JWT tokens + */ +object RudderJwtAuthenticationProvider { + + val PROTOCOL_ID: String = "oauth2ApiJwt" +} + +/* + * JWT bearer token for oauth2 protected API - authentication provider. + * This class is here only to allow to hook our mapping class in place of Spring default one. + */ +class RudderJwtAuthenticationProvider(jwtDecoder: JwtDecoder, converter: RudderJwtAuthenticationConverter) + extends AuthenticationProvider { + private val jwtAuthenticationProvider = new JwtAuthenticationProvider(jwtDecoder) + jwtAuthenticationProvider.setJwtAuthenticationConverter(converter) + + override def authenticate(authentication: Authentication): Authentication = { + val a = jwtAuthenticationProvider.authenticate(authentication) + a + } + + override def supports(authentication: Class[?]): Boolean = jwtAuthenticationProvider.supports(authentication) } class RudderJwtAuthenticationConverter( clientRegistrationRepository: Option[RudderJwtRegistration], - roleApiMapping: RoleApiMapping + roleApiMapping: RoleApiMapping, + pivotAttribute: String ) extends Converter[Jwt, AbstractAuthenticationToken] { import bootstrap.rudder.plugin.RudderJwtAuthenticationProvider.PROTOCOL_ID @@ -1154,7 +1286,7 @@ class RudderJwtAuthenticationConverter( // Find the registration for that token. It's done by looking at the client ID it must contain. // We only have the clientId, so we need to check them all - val clientId = t.getToken.getClaimAsString(RudderJwtAuthenticationConverter.CLIENT_ID_CLAIM) + val clientId = t.getToken.getClaimAsString(pivotAttribute) if (clientId == null) { // we're in Java-land, these things can happen throw new InvalidBearerTokenException( @@ -1199,7 +1331,7 @@ class RudderJwtAuthenticationConverter( ApiAccountId(jwt.getId), ApiAccountKind.PublicApi(apiAuthz, exp), ApiAccountName(jwt.getId), - ApiToken(jwt.getTokenValue), + Some(ApiToken(jwt.getTokenValue)), "", isEnabled = true, // always enabled at that point, since the token is valid created, @@ -1218,26 +1350,113 @@ class RudderJwtAuthenticationConverter( s"Principal from JWT '${details.getUsername}' final roles: [${roles.map(_.name).mkString(", ")}], and API authz: ${apiAuthz.debugString}, and tenants: ${nsc.value}" ) - RudderOAuth2Token(t, details) + RudderOAuth2Jwt(t, details) } } } } -object RudderJwtAuthenticationProvider { +/////////////// OAuth2/OIDC - Opaque bearer access token /////////////// - val PROTOCOL_ID: String = "oauth2Api" +/* + * Authentication provider for OAuth2/OIDC for protecting APIs with JWT tokens + */ +object RudderOpaqueTokenAuthenticationProvider { + val PROTOCOL_ID: String = "oauth2ApiOpaqueToken" } -class RudderJwtAuthenticationProvider(jwtDecoder: JwtDecoder, converter: RudderJwtAuthenticationConverter) - extends AuthenticationProvider { - val jwtAuthenticationProvider = new JwtAuthenticationProvider(jwtDecoder) - jwtAuthenticationProvider.setJwtAuthenticationConverter(converter) +// this class is only here to allow to use our convert in place of default spring configuration +class RudderOpaqueTokenAuthenticationProvider( + introspector: NimbusOpaqueTokenIntrospector, + converter: RudderOpaqueTokenAuthenticationConverter +) extends AuthenticationProvider { + private val opaqueTokenAuthenticationProvider = new OpaqueTokenAuthenticationProvider(introspector) + opaqueTokenAuthenticationProvider.setAuthenticationConverter(converter) override def authenticate(authentication: Authentication): Authentication = { - val a = jwtAuthenticationProvider.authenticate(authentication) + val a = opaqueTokenAuthenticationProvider.authenticate(authentication) a } - override def supports(authentication: Class[?]): Boolean = jwtAuthenticationProvider.supports(authentication) + override def supports(authentication: Class[?]): Boolean = opaqueTokenAuthenticationProvider.supports(authentication) +} + +/* + * Logic for mapping a validated opaque bearer access token to Rudder logic. + * All the authentication security is done by spring-security, here we manage mapping + * logic to rudder "user details". + */ +class RudderOpaqueTokenAuthenticationConverter( + roApiAccountRepository: RoApiAccountRepository, + pivotAttribute: String +) extends OpaqueTokenAuthenticationConverter { + + override def convert(introspectedToken: String, authenticatedPrincipal: OAuth2AuthenticatedPrincipal): Authentication = { + val t = DefaultOpaqueTokenAuthenticationConverter + .convert(introspectedToken, authenticatedPrincipal) + + // retrieve token id + val tokenId = t.getTokenAttributes.asScala.get(pivotAttribute) match { + case Some(v) => + // we only understand string for that value + v match { + case null | "" => + throw new InvalidBearerTokenException( + s"An opaque Bearer token was received but value for '${pivotAttribute}' claim isn't a non-empty string so the token is invalid" + ) + case id: String => ApiAccountId(id) + case _ => + throw new InvalidBearerTokenException( + s"An opaque Bearer token was received but value for '${pivotAttribute}' claim isn't a string so the token is invalid" + ) + } + + case None => + throw new InvalidBearerTokenException( + s"An opaque Bearer token was received but it doesn't have a '${pivotAttribute}' claim, so we don't have a token ID and the token is invalid" + ) + } + + // try to lookup token id + roApiAccountRepository.getById(tokenId).runNow match { + case None => + throw new InvalidBearerTokenException( + s"An opaque Bearer token was received but No token with ID ${tokenId.value} is configured in Rudder" + ) + case Some(apiAccount) => + if (!apiAccount.isEnabled) { + throw new InvalidBearerTokenException( + s"An opaque Bearer token was received but token with ID ${tokenId.value} is disabled in Rudder" + ) + } else { + + // we only accept public API token for that kind of authentication + apiAccount.kind match { + case ApiAccountKind.PublicApi(authz, expirationDate) => + expirationDate match { + case Some(date) if (DateTime.now().isAfter(date)) => + throw new InvalidBearerTokenException( + s"An opaque Bearer token was received but API token with ID ${tokenId.value} is expired in Rudder" + ) + + case _ => // no expiration date or expiration date not reached + val user = RudderUserDetail( + RudderAccount.Api(apiAccount), + UserStatus.Active, + RudderAuthType.Api.apiRudderRole, + authz, + apiAccount.tenants + ) + RudderOAuth2OpaqueToken(t, user) + } + + // all other API account type leads to an error + case _ => + throw new InvalidBearerTokenException( + s"An opaque Bearer token was received but No valid public API token with ID ${tokenId.value} is configured in Rudder" + ) + } + } + } + } } diff --git a/auth-backends/src/main/scala/com/normation/plugins/authbackends/Oauth2Authentication.scala b/auth-backends/src/main/scala/com/normation/plugins/authbackends/Oauth2Authentication.scala index dd934cf14..7c443ee93 100644 --- a/auth-backends/src/main/scala/com/normation/plugins/authbackends/Oauth2Authentication.scala +++ b/auth-backends/src/main/scala/com/normation/plugins/authbackends/Oauth2Authentication.scala @@ -119,20 +119,55 @@ final case class JwtAudience( // properties common to both JWT and OAuth2/OIDC registration sealed trait RudderOAuth2Registration { def registrationId: String - def roles: ProvidedRoles - def tenants: ProvidedTenants +} + +trait RegistrationWithPivotAttribute { + def pivotAttributeName: String +} + +trait RegistrationWithRoles { + def roles: ProvidedRoles + def tenants: ProvidedTenants } /* * API access with JWT (bearer token) - client_credentials workflow */ final case class RudderJwtRegistration( - registrationId: String, - jwkSetUri: String, - audience: JwtAudience, - roles: ProvidedRoles, - tenants: ProvidedTenants -) extends RudderOAuth2Registration // there is nothing secret here, no need to override `toString()`. + registrationId: String, + jwkSetUri: String, + pivotAttributeName: String, + audience: JwtAudience, + roles: ProvidedRoles, + tenants: ProvidedTenants +) extends RudderOAuth2Registration with RegistrationWithRoles + with RegistrationWithPivotAttribute // there is nothing secret here, no need to override `toString()`. + +object RudderJwtRegistration { + val defaultPivotAttribute: String = "cid" +} + +/* + * API access with JWT (bearer token) - client_credentials workflow + */ +final case class RudderOpaqueTokenRegistration( + registrationId: String, + clientId: String, + clientSecret: String, + introspectUri: String, + pivotAttributeName: String +) extends RudderOAuth2Registration with RegistrationWithPivotAttribute { + // be careful to not leak secret in "toString" + override def toString: String = { + s"""{${registrationId}}, clientId:'${clientId}' introspect URL: '${introspectUri}', token mapping attribute: ${pivotAttributeName}""" + } +} + +object RudderOpaqueTokenRegistration { + // by default, the user/service ID that gained authentication with that token is store in "sub", but it can + // be implementation specific. + val defaultPivotAttribute: String = "sub" +} /* * User login - authorization_code workflow. @@ -147,15 +182,18 @@ final case class RudderClientRegistration( roles: ProvidedRoles, provisioning: Boolean, tenants: ProvidedTenants -) extends RudderOAuth2Registration { +) extends RudderOAuth2Registration with RegistrationWithRoles with RegistrationWithPivotAttribute { override def registrationId: String = registration.getRegistrationId + override def pivotAttributeName: String = registration.getProviderDetails.getUserInfoEndpoint.getUserNameAttributeName + + // we don't have access to "registration.toString", and it leaks "clientSecret", so we need to hide it override def toString: String = { - toDebugStringWithSecret.replaceFirst("""clientSecret='([^']+?)'""", "clientSecret='*****'") + toDebugStringWithSecret.replaceAll("""clientSecret='([^']+?)'""", "clientSecret='*****'") } // avoid that in logs etc, use only for interactive debugging sessions - def toDebugStringWithSecret = + def toDebugStringWithSecret: String = s"""{${registration.toString}}, '${infoMsg}', roles: ${roles.toString}, user provisioning enabled: ${provisioning}""" } @@ -185,6 +223,7 @@ object RudderRegistrationPropertyCommon { val A_URI_JWK_SET = "uri.jwkSet" val A_URI_LOGOUT = "uri.logout" val A_URI_LOGOUT_REDIRECT = "uri.logoutRedirect" + val A_URI_INTROSPECT = "uri.introspect" val A_PIVOT_ATTRIBUTE = "userNameAttributeName" val A_ROLES_ENABLED = "roles.enabled" val A_ROLES_ATTRIBUTE = "roles.attribute" @@ -227,7 +266,8 @@ object RudderRegistrationPropertyCommon { A_URI_JWK_SET -> "provider URL to check signature of JWT token (see provider documentation)", A_URI_LOGOUT -> "(optional) provider URL to logout and end session (see provider documentation).", A_URI_LOGOUT_REDIRECT -> "(optional) the redirect URL to provide to the IdP after logout", - A_PIVOT_ATTRIBUTE -> "the attribute used to find local app user", + A_URI_INTROSPECT -> "in case of opaque access bearer token, the introspect URL on which the token must be validated", + A_PIVOT_ATTRIBUTE -> "the attribute used to find local app user (OIDC user authentication) or the local API token (opaque bearer token)", A_ROLES_ENABLED -> "(default false) enable custom role extension by OIDC", A_ROLES_ATTRIBUTE -> "the attribute to use for list of custom role name. It's content in token must be a array of strings.", A_ROLES_OVERRIDE -> "(default false) keep user configured roles in rudder-user.xml or override them with the one provided in the token", @@ -450,7 +490,7 @@ trait RudderPropertyBasedRegistration[A <: RudderOAuth2Registration] { object RudderPropertyBasedJwtRegistrationDefinition { - private val baseProperty = "rudder.auth.jwt.provider" + private val baseProperty = "rudder.auth.oauth2.jwt.provider" private val registrationLogName = "OAuth2 JWT" def make(): IOResult[RudderPropertyBasedJwtRegistrationDefinition] = { @@ -483,15 +523,17 @@ class RudderPropertyBasedJwtRegistrationDefinition(val registrations: Ref[List[( implicit val c = config for { - jwkSetUri <- read(A_URI_JWK_SET) - checkAudience <- read(A_AUDIENCE_CHECK).catchAll(_ => "true".succeed) - audienceValue <- read(A_AUDIENCE_VALUE).catchAll(_ => "io.rudder.api".succeed) - roles <- readRoles() - tenants <- readTenants() + jwkSetUri <- read(A_URI_JWK_SET) + checkAudience <- read(A_AUDIENCE_CHECK).catchAll(_ => "true".succeed) + audienceValue <- read(A_AUDIENCE_VALUE).catchAll(_ => "io.rudder.api".succeed) + pivotAttribute <- read(A_PIVOT_ATTRIBUTE).catchAll(_ => RudderJwtRegistration.defaultPivotAttribute.succeed) + roles <- readRoles() + tenants <- readTenants() } yield { RudderJwtRegistration( id, jwkSetUri, + pivotAttribute, JwtAudience(toBool(checkAudience), audienceValue), roles, tenants @@ -500,6 +542,60 @@ class RudderPropertyBasedJwtRegistrationDefinition(val registrations: Ref[List[( } } +/* + * Registration for opaque access tokens + */ +object RudderPropertyBasedOpaqueTokenRegistrationDefinition { + + private val baseProperty = "rudder.auth.oauth2.opaque.provider" + private val registrationLogName = "OAuth2 Opaque Access Token" + + def make(): IOResult[RudderPropertyBasedOpaqueTokenRegistrationDefinition] = { + for { + ref <- Ref.make(List.empty[(String, RudderOpaqueTokenRegistration)]) + } yield { + new RudderPropertyBasedOpaqueTokenRegistrationDefinition(ref) + } + } + +} + +class RudderPropertyBasedOpaqueTokenRegistrationDefinition(val registrations: Ref[List[(String, RudderOpaqueTokenRegistration)]]) + extends RudderPropertyBasedRegistration[RudderOpaqueTokenRegistration] { + + import com.normation.plugins.authbackends.RudderRegistrationPropertyCommon.* + + override def baseProperty: String = RudderPropertyBasedOpaqueTokenRegistrationDefinition.baseProperty + override def registrationLogName: String = RudderPropertyBasedOpaqueTokenRegistrationDefinition.registrationLogName + + def updateRegistration(config: Config): IOResult[Unit] = { + for { + newOnes <- readAllRegistrations(config, readOneRegistration) + _ <- registrations.set(newOnes) + } yield () + } + + def readOneRegistration(id: String, config: Config): IOResult[RudderOpaqueTokenRegistration] = { + implicit val base = BasePath(baseProperty, id) + implicit val c = config + + for { + clientId <- read(A_CLIENT_ID) + clientSecret <- read(A_CLIENT_SECRET) + introspectUri <- read(A_URI_INTROSPECT) + pivotAttribute <- read(A_PIVOT_ATTRIBUTE).catchAll(_ => RudderOpaqueTokenRegistration.defaultPivotAttribute.succeed) + } yield { + RudderOpaqueTokenRegistration( + id, + clientId, + clientSecret, + introspectUri, + pivotAttribute + ) + } + } +} + /* * Client registration definition based on rudder property file: * - read property `rudder.auth.oauth2.client.registrations` which give a comma-separated diff --git a/auth-backends/src/main/scala/org/springframework/security/oauth2/server/resource/authentication/DefaultOpaqueTokenAuthenticationConverter.scala b/auth-backends/src/main/scala/org/springframework/security/oauth2/server/resource/authentication/DefaultOpaqueTokenAuthenticationConverter.scala new file mode 100644 index 000000000..9b7b0dc91 --- /dev/null +++ b/auth-backends/src/main/scala/org/springframework/security/oauth2/server/resource/authentication/DefaultOpaqueTokenAuthenticationConverter.scala @@ -0,0 +1,51 @@ +/* + ************************************************************************************* + * Copyright 2025 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 org.springframework.security.oauth2.server.resource.authentication + +import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal + +/* + * This class exists only to be able to access Spring default implementation + * of opaque bearer token convert which exists in `OpaqueTokenAuthenticationProvider` + * as a package private static method. + */ + +object DefaultOpaqueTokenAuthenticationConverter { + def convert(introspectedToken: String, authenticatedPrincipal: OAuth2AuthenticatedPrincipal): BearerTokenAuthentication = + OpaqueTokenAuthenticationProvider.convert(introspectedToken, authenticatedPrincipal) +} diff --git a/auth-backends/src/test/resources/jwt/jwt_reverse_role_mapping.properties b/auth-backends/src/test/resources/jwt/jwt_reverse_role_mapping.properties index 603191b6c..21b39d979 100644 --- a/auth-backends/src/test/resources/jwt/jwt_reverse_role_mapping.properties +++ b/auth-backends/src/test/resources/jwt/jwt_reverse_role_mapping.properties @@ -1,18 +1,18 @@ -rudder.auth.jwt.provider.registrations=someidp -rudder.auth.jwt.provider.someidp.name=Some ID -rudder.auth.jwt.provider.someidp.uri.jwkSet="https://someidp/oauth2/v1/keys" -rudder.auth.jwt.provider.someidp.audience=io.rudder.api -rudder.auth.jwt.provider.someidp.roles.attribute=customroles -rudder.auth.jwt.provider.someidp.roles.mapping.enforced=true -rudder.auth.jwt.provider.someidp.roles.mapping.entitlements.rudder_admin=administrator -rudder.auth.jwt.provider.someidp.roles.mapping.entitlements.rudder_readonly=readonly -rudder.auth.jwt.provider.someidp.roles.mapping.reverseEntitlements.readonlyOVERRIDDEN=rudder_readonly -rudder.auth.jwt.provider.someidp.roles.mapping.reverseEntitlements.administrator=CN=AAAA-BBBBB,OU=Groups,OU=_IT,OU=BB-DD,OU=UUU-XXXX-YY,DC=ee,DC=if,DC=ttttt,DC=uuu -rudder.auth.jwt.provider.someidp.tenants.enabled=true -rudder.auth.jwt.provider.someidp.tenants.attribute=customtenants -rudder.auth.jwt.provider.someidp.tenants.override=true -rudder.auth.jwt.provider.someidp.tenants.mapping.enforced=true -rudder.auth.jwt.provider.someidp.tenants.mapping.entitlements.rudder_TA=TA -rudder.auth.jwt.provider.someidp.tenants.mapping.entitlements.rudder_TB=TB -rudder.auth.jwt.provider.someidp.tenants.mapping.reverseEntitlements.TB_OVERRIDDEN=rudder_TB -rudder.auth.jwt.provider.someidp.tenants.mapping.reverseEntitlements.TA=CN=AAAA-BBBBB,OU=Groups,OU=_IT,OU=BB-DD,OU=UUU-XXXX-YY,DC=ee,DC=if,DC=ttttt,DC=uuu +rudder.auth.oauth2.jwt.provider.registrations=someidp +rudder.auth.oauth2.jwt.provider.someidp.name=Some ID +rudder.auth.oauth2.jwt.provider.someidp.uri.jwkSet="https://someidp/oauth2/v1/keys" +rudder.auth.oauth2.jwt.provider.someidp.audience=io.rudder.api +rudder.auth.oauth2.jwt.provider.someidp.roles.attribute=customroles +rudder.auth.oauth2.jwt.provider.someidp.roles.mapping.enforced=true +rudder.auth.oauth2.jwt.provider.someidp.roles.mapping.entitlements.rudder_admin=administrator +rudder.auth.oauth2.jwt.provider.someidp.roles.mapping.entitlements.rudder_readonly=readonly +rudder.auth.oauth2.jwt.provider.someidp.roles.mapping.reverseEntitlements.readonlyOVERRIDDEN=rudder_readonly +rudder.auth.oauth2.jwt.provider.someidp.roles.mapping.reverseEntitlements.administrator=CN=AAAA-BBBBB,OU=Groups,OU=_IT,OU=BB-DD,OU=UUU-XXXX-YY,DC=ee,DC=if,DC=ttttt,DC=uuu +rudder.auth.oauth2.jwt.provider.someidp.tenants.enabled=true +rudder.auth.oauth2.jwt.provider.someidp.tenants.attribute=customtenants +rudder.auth.oauth2.jwt.provider.someidp.tenants.override=true +rudder.auth.oauth2.jwt.provider.someidp.tenants.mapping.enforced=true +rudder.auth.oauth2.jwt.provider.someidp.tenants.mapping.entitlements.rudder_TA=TA +rudder.auth.oauth2.jwt.provider.someidp.tenants.mapping.entitlements.rudder_TB=TB +rudder.auth.oauth2.jwt.provider.someidp.tenants.mapping.reverseEntitlements.TB_OVERRIDDEN=rudder_TB +rudder.auth.oauth2.jwt.provider.someidp.tenants.mapping.reverseEntitlements.TA=CN=AAAA-BBBBB,OU=Groups,OU=_IT,OU=BB-DD,OU=UUU-XXXX-YY,DC=ee,DC=if,DC=ttttt,DC=uuu diff --git a/auth-backends/src/test/resources/jwt/jwt_simple.properties b/auth-backends/src/test/resources/jwt/jwt_simple.properties index b93bad1f5..9f0e4624d 100644 --- a/auth-backends/src/test/resources/jwt/jwt_simple.properties +++ b/auth-backends/src/test/resources/jwt/jwt_simple.properties @@ -1,14 +1,14 @@ -rudder.auth.jwt.provider.registrations=someidp -rudder.auth.jwt.provider.someidp.name=Some ID -rudder.auth.jwt.provider.someidp.uri.jwkSet="https://someidp/oauth2/v1/keys" -rudder.auth.jwt.provider.someidp.audience=io.rudder.api -rudder.auth.jwt.provider.someidp.roles.attribute=customroles -rudder.auth.jwt.provider.someidp.roles.mapping.enforced=true -rudder.auth.jwt.provider.someidp.roles.mapping.entitlements.rudder_admin=administrator -rudder.auth.jwt.provider.someidp.roles.mapping.entitlements.rudder_readonly=readonly -rudder.auth.jwt.provider.someidp.tenants.enabled=true -rudder.auth.jwt.provider.someidp.tenants.attribute=customtenants -rudder.auth.jwt.provider.someidp.tenants.override=true -rudder.auth.jwt.provider.someidp.tenants.mapping.enforced=true -rudder.auth.jwt.provider.someidp.tenants.mapping.entitlements.rudder_TA=TA -rudder.auth.jwt.provider.someidp.tenants.mapping.entitlements.rudder_TB=TB +rudder.auth.oauth2.jwt.provider.registrations=someidp +rudder.auth.oauth2.jwt.provider.someidp.name=Some ID +rudder.auth.oauth2.jwt.provider.someidp.uri.jwkSet="https://someidp/oauth2/v1/keys" +rudder.auth.oauth2.jwt.provider.someidp.audience=io.rudder.api +rudder.auth.oauth2.jwt.provider.someidp.roles.attribute=customroles +rudder.auth.oauth2.jwt.provider.someidp.roles.mapping.enforced=true +rudder.auth.oauth2.jwt.provider.someidp.roles.mapping.entitlements.rudder_admin=administrator +rudder.auth.oauth2.jwt.provider.someidp.roles.mapping.entitlements.rudder_readonly=readonly +rudder.auth.oauth2.jwt.provider.someidp.tenants.enabled=true +rudder.auth.oauth2.jwt.provider.someidp.tenants.attribute=customtenants +rudder.auth.oauth2.jwt.provider.someidp.tenants.override=true +rudder.auth.oauth2.jwt.provider.someidp.tenants.mapping.enforced=true +rudder.auth.oauth2.jwt.provider.someidp.tenants.mapping.entitlements.rudder_TA=TA +rudder.auth.oauth2.jwt.provider.someidp.tenants.mapping.entitlements.rudder_TB=TB diff --git a/auth-backends/src/test/resources/opaque/opaque.properties b/auth-backends/src/test/resources/opaque/opaque.properties new file mode 100644 index 000000000..ba4a06d03 --- /dev/null +++ b/auth-backends/src/test/resources/opaque/opaque.properties @@ -0,0 +1,5 @@ +rudder.auth.oauth2.opaque.provider.registrations=someidp +rudder.auth.oauth2.opaque.provider.someidp.client.id=xxxClientIdxxx +rudder.auth.oauth2.opaque.provider.someidp.client.secret=xxxClientPassxxx +rudder.auth.oauth2.opaque.provider.someidp.uri.introspect=https://someidp/oauth2/v1/introspect +rudder.auth.oauth2.opaque.provider.someidp.userNameAttributeName=email diff --git a/auth-backends/src/test/resources/opaque/opaque_default.properties b/auth-backends/src/test/resources/opaque/opaque_default.properties new file mode 100644 index 000000000..707baff24 --- /dev/null +++ b/auth-backends/src/test/resources/opaque/opaque_default.properties @@ -0,0 +1,4 @@ +rudder.auth.oauth2.opaque.provider.registrations=someidp +rudder.auth.oauth2.opaque.provider.someidp.client.id=xxxClientIdxxx +rudder.auth.oauth2.opaque.provider.someidp.client.secret=xxxClientPassxxx +rudder.auth.oauth2.opaque.provider.someidp.uri.introspect=https://someidp/oauth2/v1/introspect diff --git a/auth-backends/src/test/scala/com/normation/plugins/authbackends/TestReadOidcConfig.scala b/auth-backends/src/test/scala/com/normation/plugins/authbackends/TestReadOidcConfig.scala index f2c6849b7..86f52c190 100644 --- a/auth-backends/src/test/scala/com/normation/plugins/authbackends/TestReadOidcConfig.scala +++ b/auth-backends/src/test/scala/com/normation/plugins/authbackends/TestReadOidcConfig.scala @@ -176,4 +176,33 @@ class TestReadOidcConfig extends Specification { ) } } + + "reading an opaque configuration" should { + val reg = RudderOpaqueTokenRegistration( + "someidp", + "xxxClientIdxxx", + "xxxClientPassxxx", + "https://someidp/oauth2/v1/introspect", + "email" + ) + val registration = RudderPropertyBasedOpaqueTokenRegistrationDefinition.make().runNow + + "read the correct configuration" in { + + val config = ConfigFactory.parseResources("opaque/opaque.properties") + val regs = registration.readAllRegistrations(config, registration.readOneRegistration).runNow.toMap + + regs.keySet === Set("someidp") and regs("someidp") === reg + } + + "read the correct configuration with default" in { + + val config = ConfigFactory.parseResources("opaque/opaque_default.properties") + val regs = registration.readAllRegistrations(config, registration.readOneRegistration).runNow.toMap + + regs.keySet === Set("someidp") and regs("someidp") === reg.copy(pivotAttributeName = "client_id") + } + + } + } From 0acaa0378d7bc68fbfded03ca418e88a21e6812a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Membr=C3=A9?= Date: Thu, 6 Feb 2025 11:48:25 +0100 Subject: [PATCH 18/65] Fixes #26313: Change path to webapp logfile in plugins documentation --- auth-backends/README.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/auth-backends/README.adoc b/auth-backends/README.adoc index b6f012e8e..46847e54c 100644 --- a/auth-backends/README.adoc +++ b/auth-backends/README.adoc @@ -436,7 +436,7 @@ It was an AD error that seems to have been triggered by some unexpected request If you want to connect with a secure connection to an LDAP or AD, you need to add the directory certificate to Rudder's JVM `keystore`. -Without that, you will see errors in `/var/log/rudder/webapp/XXXXXXX_stderrout.log` files like: +Without that, you will see errors in `/var/log/rudder/webapp/webapp.log` files like: ``` WARN application - Login authentication failed for user 'xxx' from IP '127.0.0.1|X-Forwarded-For:xxx.xxx.xxx.xxx': simple bind failed: xxx.xxx:636; nested exception is javax.naming.CommunicationException: simple bind failed: @@ -699,7 +699,7 @@ Here are some guidelines to help address possible configuration problems. Check that you correctly updated parameter `rudder.auth.provider` to include `oidc` or `oauth2` in the list, that you have at least one key defined in `rudder.auth.oauth2.provider.registrations`, and -that you have Rudder webapp logs (`/var/log/rudder/webapp/YYYY_mm_dd.stderrout.log`) lines like: +that you have Rudder webapp logs (`/var/log/rudder/webapp/webapp.log`) lines like: ---- [timestamp] INFO application - Configured authentication provider(s): [rootAdmin, oidc, file] From cca723c9f5b562d78650401d55ceb7f331774c0f Mon Sep 17 00:00:00 2001 From: Clark Andrianasolo Date: Thu, 6 Feb 2025 15:27:56 +0100 Subject: [PATCH 19/65] Fixes #26316: Fix clear text token chimney implicits and default pivot attribute --- .../plugins/apiauthorizations/UserTokenApiDefinition.scala | 4 ++-- .../normation/plugins/authbackends/TestReadOidcConfig.scala | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api-authorizations/src/main/scala/com/normation/plugins/apiauthorizations/UserTokenApiDefinition.scala b/api-authorizations/src/main/scala/com/normation/plugins/apiauthorizations/UserTokenApiDefinition.scala index 0d677ffc3..7934b33e5 100644 --- a/api-authorizations/src/main/scala/com/normation/plugins/apiauthorizations/UserTokenApiDefinition.scala +++ b/api-authorizations/src/main/scala/com/normation/plugins/apiauthorizations/UserTokenApiDefinition.scala @@ -51,6 +51,7 @@ import com.normation.rudder.users.UserRepository import com.normation.utils.DateFormaterService import com.normation.utils.StringUuidGenerator import io.scalaland.chimney.Transformer +import io.scalaland.chimney.syntax.* import net.liftweb.http.LiftResponse import net.liftweb.http.Req import org.joda.time.DateTime @@ -212,7 +213,6 @@ object UserApiImpl { object RestApiAccount extends UserJsonCodec { implicit class ApiAccountOps(val account: ApiAccount) extends AnyVal { import ApiAccountKind.* - import io.scalaland.chimney.syntax.* def expirationDate: Option[String] = { account.kind match { case PublicApi(_, expirationDate) => expirationDate.map(DateFormaterService.getDisplayDateTimePicker) @@ -254,7 +254,7 @@ object UserApiImpl { implicit val transformer: Transformer[ApiAccount, RestApiAccount] = Transformer .define[ApiAccount, RestApiAccount] - .withFieldComputed(_.token, a => ClearTextToken(a.token.map(_.value).getOrElse(""))) + .withFieldComputedFrom(_.token)(_.token, _.fold(ClearTextToken(""))(_.transformInto[ClearTextToken])) .withFieldComputed(_.kind, _.kind.kind) .withFieldComputed(_.acl, _.acl) .withFieldComputed(_.expirationDate, _.expirationDate) diff --git a/auth-backends/src/test/scala/com/normation/plugins/authbackends/TestReadOidcConfig.scala b/auth-backends/src/test/scala/com/normation/plugins/authbackends/TestReadOidcConfig.scala index 86f52c190..b775a8d56 100644 --- a/auth-backends/src/test/scala/com/normation/plugins/authbackends/TestReadOidcConfig.scala +++ b/auth-backends/src/test/scala/com/normation/plugins/authbackends/TestReadOidcConfig.scala @@ -200,7 +200,7 @@ class TestReadOidcConfig extends Specification { val config = ConfigFactory.parseResources("opaque/opaque_default.properties") val regs = registration.readAllRegistrations(config, registration.readOneRegistration).runNow.toMap - regs.keySet === Set("someidp") and regs("someidp") === reg.copy(pivotAttributeName = "client_id") + regs.keySet === Set("someidp") and regs("someidp") === reg.copy(pivotAttributeName = "sub") } } From c384212c261ce5271c8aaa0dea00cea4ab84cbe4 Mon Sep 17 00:00:00 2001 From: "Francois @fanf42 Armand" Date: Tue, 11 Feb 2025 10:33:55 +0100 Subject: [PATCH 20/65] Fixes #26341: Impact of ApiAccount API changes on plugin --- .../apiauthorizations/UserJsonCodec.scala | 23 ------------------- .../UserTokenApiDefinition.scala | 8 +++++-- 2 files changed, 6 insertions(+), 25 deletions(-) delete mode 100644 api-authorizations/src/main/scala/com/normation/plugins/apiauthorizations/UserJsonCodec.scala diff --git a/api-authorizations/src/main/scala/com/normation/plugins/apiauthorizations/UserJsonCodec.scala b/api-authorizations/src/main/scala/com/normation/plugins/apiauthorizations/UserJsonCodec.scala deleted file mode 100644 index bf0cba355..000000000 --- a/api-authorizations/src/main/scala/com/normation/plugins/apiauthorizations/UserJsonCodec.scala +++ /dev/null @@ -1,23 +0,0 @@ -package com.normation.plugins.apiauthorizations - -import com.normation.rudder.api.ApiAccountId -import com.normation.rudder.api.ApiAccountName -import com.normation.rudder.api.ApiAccountType -import com.normation.rudder.api.ApiAuthorizationKind -import com.normation.rudder.apidata.JsonApiAcl -import com.normation.utils.DateFormaterService -import org.joda.time.DateTime -import zio.json.* - -trait UserJsonCodec { - - implicit val accountIdEncoder: JsonEncoder[ApiAccountId] = JsonEncoder[String].contramap(_.value) - implicit val accountNameEncoder: JsonEncoder[ApiAccountName] = JsonEncoder[String].contramap(_.value) - implicit val dateTimeEncoder: JsonEncoder[DateTime] = JsonEncoder[String].contramap(DateFormaterService.serialize) - implicit val accountTypeEncoder: JsonEncoder[ApiAccountType] = JsonEncoder[String].contramap(_.name) - implicit val authorizationTypeEncoder: JsonEncoder[ApiAuthorizationKind] = JsonEncoder[String].contramap(_.name) - implicit val aclEncoder: JsonEncoder[JsonApiAcl] = DeriveJsonEncoder.gen[JsonApiAcl] - -} - -object UserJsonCodec extends UserJsonCodec diff --git a/api-authorizations/src/main/scala/com/normation/plugins/apiauthorizations/UserTokenApiDefinition.scala b/api-authorizations/src/main/scala/com/normation/plugins/apiauthorizations/UserTokenApiDefinition.scala index 7934b33e5..ed57baaef 100644 --- a/api-authorizations/src/main/scala/com/normation/plugins/apiauthorizations/UserTokenApiDefinition.scala +++ b/api-authorizations/src/main/scala/com/normation/plugins/apiauthorizations/UserTokenApiDefinition.scala @@ -38,6 +38,7 @@ package com.normation.plugins.apiauthorizations import bootstrap.liftweb.AuthBackendProvidersManager + import com.normation.errors.* import com.normation.eventlog.ModificationId import com.normation.rudder.api.* @@ -45,16 +46,19 @@ import com.normation.rudder.apidata.JsonApiAcl import com.normation.rudder.facts.nodes.NodeSecurityContext import com.normation.rudder.rest.* import com.normation.rudder.rest.UserApi +import com.normation.rudder.rest.data.ApiAccountCodec import com.normation.rudder.rest.implicits.ToLiftResponseOne import com.normation.rudder.rest.lift.* import com.normation.rudder.users.UserRepository import com.normation.utils.DateFormaterService import com.normation.utils.StringUuidGenerator + import io.scalaland.chimney.Transformer import io.scalaland.chimney.syntax.* import net.liftweb.http.LiftResponse import net.liftweb.http.Req import org.joda.time.DateTime + import zio.json.* class UserApiImpl( @@ -210,7 +214,7 @@ object UserApiImpl { acl: Option[List[JsonApiAcl]] ) - object RestApiAccount extends UserJsonCodec { + object RestApiAccount extends ApiAccountCodec { implicit class ApiAccountOps(val account: ApiAccount) extends AnyVal { import ApiAccountKind.* def expirationDate: Option[String] = { @@ -313,7 +317,7 @@ object UserApiImpl { accounts: RestAccountId ) - object RestAccountIdResponse extends UserJsonCodec { + object RestAccountIdResponse extends ApiAccountCodec { implicit val accountIdResponseEncoder: JsonEncoder[RestAccountId] = DeriveJsonEncoder.gen[RestAccountId] implicit val encoder: JsonEncoder[RestAccountIdResponse] = DeriveJsonEncoder.gen[RestAccountIdResponse] From f56ecb06a7a0841c159ecc2616f2190bac384c0e Mon Sep 17 00:00:00 2001 From: Clark Andrianasolo Date: Thu, 13 Feb 2025 11:27:05 +0100 Subject: [PATCH 21/65] Fixes #26349: Impact of plugins datastructures splitting in plugins --- .../plugins/authbackends/snippet/Oauth2LoginBanner.scala | 4 ++-- .../normation/plugins/branding/snippet/LoginBranding.scala | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/auth-backends/src/main/scala/com/normation/plugins/authbackends/snippet/Oauth2LoginBanner.scala b/auth-backends/src/main/scala/com/normation/plugins/authbackends/snippet/Oauth2LoginBanner.scala index be0f6015e..8d569e931 100644 --- a/auth-backends/src/main/scala/com/normation/plugins/authbackends/snippet/Oauth2LoginBanner.scala +++ b/auth-backends/src/main/scala/com/normation/plugins/authbackends/snippet/Oauth2LoginBanner.scala @@ -40,7 +40,7 @@ package com.normation.plugins.authbackends.snippet import bootstrap.rudder.plugin.AuthBackendsConf import com.normation.plugins.PluginExtensionPoint import com.normation.plugins.PluginStatus -import com.normation.plugins.PluginVersion +import com.normation.plugins.RudderPluginVersion import com.normation.plugins.authbackends.LoginFormRendering import com.normation.plugins.authbackends.RudderPropertyBasedOAuth2RegistrationDefinition import com.normation.rudder.web.snippet.Login @@ -53,7 +53,7 @@ import scala.xml.NodeSeq class Oauth2LoginBanner( val status: PluginStatus, - version: PluginVersion, + version: RudderPluginVersion, registrations: RudderPropertyBasedOAuth2RegistrationDefinition )(implicit val ttag: ClassTag[Login]) extends PluginExtensionPoint[Login] { diff --git a/branding/src/main/scala/com/normation/plugins/branding/snippet/LoginBranding.scala b/branding/src/main/scala/com/normation/plugins/branding/snippet/LoginBranding.scala index f7b3a2243..e45cf2c3b 100644 --- a/branding/src/main/scala/com/normation/plugins/branding/snippet/LoginBranding.scala +++ b/branding/src/main/scala/com/normation/plugins/branding/snippet/LoginBranding.scala @@ -40,7 +40,7 @@ package com.normation.plugins.branding.snippet import bootstrap.rudder.plugin.BrandingPluginConf import com.normation.plugins.PluginExtensionPoint import com.normation.plugins.PluginStatus -import com.normation.plugins.PluginVersion +import com.normation.plugins.RudderPluginVersion import com.normation.rudder.web.snippet.Login import com.normation.zio.UnsafeRun import net.liftweb.common.Loggable @@ -48,7 +48,7 @@ import net.liftweb.util.Helpers.* import scala.reflect.ClassTag import scala.xml.NodeSeq -class LoginBranding(val status: PluginStatus, version: PluginVersion)(implicit val ttag: ClassTag[Login]) +class LoginBranding(val status: PluginStatus, version: RudderPluginVersion)(implicit val ttag: ClassTag[Login]) extends PluginExtensionPoint[Login] with Loggable { def pluginCompose(snippet: Login): Map[String, NodeSeq => NodeSeq] = Map( From fe9658dddc2d76e3c0b343c3a925f1d31f3de890 Mon Sep 17 00:00:00 2001 From: Clark Andrianasolo Date: Fri, 14 Feb 2025 12:16:31 +0100 Subject: [PATCH 22/65] Fixes #26356: Impact of plugins datastructures splitting in plugins-private --- .../plugins/ParsePluginLicense.scala | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/plugins-common-private/src/main/scala/com/normation/plugins/ParsePluginLicense.scala b/plugins-common-private/src/main/scala/com/normation/plugins/ParsePluginLicense.scala index f37578144..703c52f28 100644 --- a/plugins-common-private/src/main/scala/com/normation/plugins/ParsePluginLicense.scala +++ b/plugins-common-private/src/main/scala/com/normation/plugins/ParsePluginLicense.scala @@ -40,6 +40,8 @@ package com.normation.plugins import com.normation.license.* import com.normation.license.MaybeLicenseError.Maybe import com.normation.rudder.domain.logger.PluginLogger +import io.scalaland.chimney.Transformer +import io.scalaland.chimney.syntax.* import java.nio.file.Files import java.nio.file.Paths import java.nio.file.attribute.FileTime @@ -52,6 +54,7 @@ import scala.util.control.NonFatal */ trait LicensedPluginCheck extends PluginStatus { + import LicensedPluginCheck.* /* * implementation must define variable with the following maven properties @@ -117,7 +120,7 @@ trait LicensedPluginCheck extends PluginStatus { infoCache } - def current: PluginStatusInfo = { + def current: RudderPluginLicenseStatus = { (for { info <- maybeLicense (license, version) = info @@ -125,21 +128,35 @@ trait LicensedPluginCheck extends PluginStatus { } yield { check }) match { - case Right(x) => PluginStatusInfo.EnabledWithLicense(licenseInformation(x)) - case Left(y) => PluginStatusInfo.Disabled(y.msg, maybeLicense.toOption.map { case (l, v) => licenseInformation(l) }) + case Right(x) => RudderPluginLicenseStatus.EnabledWithLicense(x.content.transformInto[PluginLicense]) + case Left(y) => + RudderPluginLicenseStatus.Disabled( + y.msg, + maybeLicense.toOption.map { case (l, _) => l.content.transformInto[PluginLicense] } + ) } } +} - private[this] def licenseInformation(l: License): PluginLicenseInfo = { - PluginLicenseInfo( - licensee = l.content.licensee.value, - softwareId = l.content.softwareId.value, - minVersion = l.content.minVersion.value.toString, - maxVersion = l.content.maxVersion.value.toString, - startDate = l.content.startDate.value, - endDate = l.content.endDate.value, - maxNodes = l.content.maxNodes.value, - others = l.content.others.map(_.raw).toMap - ) +// LicenseInformation has exactly the fields in license with different wrapper types : define transformers +private object LicensedPluginCheck { + import com.normation.utils.DateFormaterService.JodaTimeToJava + + // Required for min-max version which is a parsed version + // The license defines a version with a .toString method + implicit val transformerVersion: Transformer[Version, String] = _.toString + + implicit val transformerLicensee: Transformer[LicenseField.Licensee, Licensee] = Transformer.derive + implicit val transformerSoftwareId: Transformer[LicenseField.SoftwareId, SoftwareId] = Transformer.derive + implicit val transformerMinVersion: Transformer[LicenseField.MinVersion, MinVersion] = Transformer.derive + implicit val transformerMaxVersion: Transformer[LicenseField.MaxVersion, MaxVersion] = Transformer.derive + implicit val transformerMaxNodes: Transformer[LicenseField.MaxNodes, MaxNodes] = Transformer.derive + + implicit val transformerPluginLicense: Transformer[LicenseInformation, PluginLicense] = { + Transformer + .define[LicenseInformation, PluginLicense] + .withFieldComputed(_.startDate, _.startDate.value.toJava) + .withFieldComputed(_.endDate, _.endDate.value.toJava) + .buildTransformer } } From d1ad097985b4402f9fc6e2d2a617765b2959db16 Mon Sep 17 00:00:00 2001 From: Fdall Date: Fri, 14 Feb 2025 16:04:23 +0100 Subject: [PATCH 23/65] Fixes #26361: Add a new requires_license field in the metadata in rudder-plugins --- api-authorizations/packaging/metadata | 3 ++- auth-backends/packaging/metadata | 3 ++- change-validation/packaging/metadata | 3 ++- datasources/packaging/metadata | 3 ++- scale-out-relay/packaging/metadata | 3 ++- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/api-authorizations/packaging/metadata b/api-authorizations/packaging/metadata index 01a87a15f..4b07df00b 100644 --- a/api-authorizations/packaging/metadata +++ b/api-authorizations/packaging/metadata @@ -8,5 +8,6 @@ "jar-files": [ "/opt/rudder/share/plugins/${plugin-name}/${plugin-name}.jar" ], "content": { "files.txz": "/opt/rudder/share/plugins" - } + }, + "requires_license": true } diff --git a/auth-backends/packaging/metadata b/auth-backends/packaging/metadata index 5bde8255d..c91eb08b1 100644 --- a/auth-backends/packaging/metadata +++ b/auth-backends/packaging/metadata @@ -8,5 +8,6 @@ "jar-files": [ "/opt/rudder/share/plugins/${plugin-name}/${plugin-name}.jar" ], "content": { "files.txz": "/opt/rudder/share/plugins" - } + }, + "requires_license": true } diff --git a/change-validation/packaging/metadata b/change-validation/packaging/metadata index 206921d9e..94d289e95 100644 --- a/change-validation/packaging/metadata +++ b/change-validation/packaging/metadata @@ -8,5 +8,6 @@ "jar-files": [ "/opt/rudder/share/plugins/${plugin-name}/${plugin-name}.jar" ], "content": { "files.txz": "/opt/rudder/share/plugins" - } + }, + "requires_license": true } diff --git a/datasources/packaging/metadata b/datasources/packaging/metadata index 2f98c188c..30fad468d 100644 --- a/datasources/packaging/metadata +++ b/datasources/packaging/metadata @@ -8,5 +8,6 @@ "jar-files": [ "/opt/rudder/share/plugins/${plugin-name}/${plugin-name}.jar" ], "content": { "files.txz": "/opt/rudder/share/plugins" - } + }, + "requires_license": true } diff --git a/scale-out-relay/packaging/metadata b/scale-out-relay/packaging/metadata index d89f4c6e1..d535b597c 100644 --- a/scale-out-relay/packaging/metadata +++ b/scale-out-relay/packaging/metadata @@ -8,5 +8,6 @@ "jar-files": [ "/opt/rudder/share/plugins/${plugin-name}/${plugin-name}.jar" ], "content": { "files.txz": "/opt/rudder/share/plugins" - } + }, + "requires_license": true } From 500afd12ed52a41291a58662fe292e92d1185973 Mon Sep 17 00:00:00 2001 From: "Francois @fanf42 Armand" Date: Wed, 19 Feb 2025 21:34:36 +0100 Subject: [PATCH 24/65] Fixes #26397: Impact of 24872 (API rights) on public plugins --- .../snippet/Oauth2LoginBanner.scala | 4 +-- .../branding/snippet/LoginBranding.scala | 4 +-- .../rudder/rest/BrandingApiSchema.scala | 6 +++-- .../api/SupervisedTargetsApi.scala | 3 ++- .../api/ValidatedUserApi.scala | 4 ++- .../ValidatedUserJdbcRepositoryTest.scala | 4 +-- .../datasources/api/DataSourceApi.scala | 27 ++++++++++++------- .../scaleoutrelay/api/ScaleOutRelayApi.scala | 3 +++ 8 files changed, 36 insertions(+), 19 deletions(-) diff --git a/auth-backends/src/main/scala/com/normation/plugins/authbackends/snippet/Oauth2LoginBanner.scala b/auth-backends/src/main/scala/com/normation/plugins/authbackends/snippet/Oauth2LoginBanner.scala index 8d569e931..be0f6015e 100644 --- a/auth-backends/src/main/scala/com/normation/plugins/authbackends/snippet/Oauth2LoginBanner.scala +++ b/auth-backends/src/main/scala/com/normation/plugins/authbackends/snippet/Oauth2LoginBanner.scala @@ -40,7 +40,7 @@ package com.normation.plugins.authbackends.snippet import bootstrap.rudder.plugin.AuthBackendsConf import com.normation.plugins.PluginExtensionPoint import com.normation.plugins.PluginStatus -import com.normation.plugins.RudderPluginVersion +import com.normation.plugins.PluginVersion import com.normation.plugins.authbackends.LoginFormRendering import com.normation.plugins.authbackends.RudderPropertyBasedOAuth2RegistrationDefinition import com.normation.rudder.web.snippet.Login @@ -53,7 +53,7 @@ import scala.xml.NodeSeq class Oauth2LoginBanner( val status: PluginStatus, - version: RudderPluginVersion, + version: PluginVersion, registrations: RudderPropertyBasedOAuth2RegistrationDefinition )(implicit val ttag: ClassTag[Login]) extends PluginExtensionPoint[Login] { diff --git a/branding/src/main/scala/com/normation/plugins/branding/snippet/LoginBranding.scala b/branding/src/main/scala/com/normation/plugins/branding/snippet/LoginBranding.scala index e45cf2c3b..f7b3a2243 100644 --- a/branding/src/main/scala/com/normation/plugins/branding/snippet/LoginBranding.scala +++ b/branding/src/main/scala/com/normation/plugins/branding/snippet/LoginBranding.scala @@ -40,7 +40,7 @@ package com.normation.plugins.branding.snippet import bootstrap.rudder.plugin.BrandingPluginConf import com.normation.plugins.PluginExtensionPoint import com.normation.plugins.PluginStatus -import com.normation.plugins.RudderPluginVersion +import com.normation.plugins.PluginVersion import com.normation.rudder.web.snippet.Login import com.normation.zio.UnsafeRun import net.liftweb.common.Loggable @@ -48,7 +48,7 @@ import net.liftweb.util.Helpers.* import scala.reflect.ClassTag import scala.xml.NodeSeq -class LoginBranding(val status: PluginStatus, version: RudderPluginVersion)(implicit val ttag: ClassTag[Login]) +class LoginBranding(val status: PluginStatus, version: PluginVersion)(implicit val ttag: ClassTag[Login]) extends PluginExtensionPoint[Login] with Loggable { def pluginCompose(snippet: Login): Map[String, NodeSeq => NodeSeq] = Map( diff --git a/branding/src/main/scala/com/normation/rudder/rest/BrandingApiSchema.scala b/branding/src/main/scala/com/normation/rudder/rest/BrandingApiSchema.scala index 29cc580c7..6896e71ce 100644 --- a/branding/src/main/scala/com/normation/rudder/rest/BrandingApiSchema.scala +++ b/branding/src/main/scala/com/normation/rudder/rest/BrandingApiSchema.scala @@ -63,14 +63,16 @@ object BrandingApiEndpoints extends Enum[BrandingApiSchema] with ApiModuleProvid val z = implicitly[Line].value val description = "Update branding plugin configuration" val (action, path) = POST / "branding" - val dataContainer: Option[String] = None + val dataContainer: Option[String] = None + val authz: List[AuthorizationType] = AuthorizationType.Administration.Write :: Nil } final case object ReloadBrandingConf extends BrandingApiSchema with ZeroParam with StartsAtVersion10 with SortIndex { val z = implicitly[Line].value val description = "Reload branding plugin configuration from config file" val (action, path) = POST / "branding" / "reload" - val dataContainer: Option[String] = None + val dataContainer: Option[String] = None + val authz: List[AuthorizationType] = AuthorizationType.Administration.Write :: Nil } def endpoints = values.toList.sortBy(_.z) diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/api/SupervisedTargetsApi.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/api/SupervisedTargetsApi.scala index 3a0fa16b3..5ef25efe4 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/api/SupervisedTargetsApi.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/api/SupervisedTargetsApi.scala @@ -88,7 +88,8 @@ object SupervisedTargetsApi extends Enum[SupervisedTargetsApi] with ApiMod val description = "Save the updated list of groups" val (action, path) = POST / "changevalidation" / "supervised" / "targets" - override def dataContainer: Option[String] = None + override def dataContainer: Option[String] = None + val authz: List[AuthorizationType] = AuthorizationType.Administration.Write :: Nil } def endpoints = values.toList.sortBy(_.z) diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/api/ValidatedUserApi.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/api/ValidatedUserApi.scala index 3fa096aaa..b42d52eee 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/api/ValidatedUserApi.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/api/ValidatedUserApi.scala @@ -77,7 +77,8 @@ object ValidatedUserApi extends Enum[ValidatedUserApi] with ApiModuleProvi val (action, path) = DELETE / "validatedUsers" / "{username}" override val name = "removeValidatedUser" - override def dataContainer: Option[String] = None + override def dataContainer: Option[String] = None + val authz: List[AuthorizationType] = AuthorizationType.Administration.Write :: Nil } final case object SaveWorkflowUsers extends ValidatedUserApi with ZeroParam with StartsAtVersion3 with SortIndex { val z = implicitly[Line].value @@ -86,6 +87,7 @@ object ValidatedUserApi extends Enum[ValidatedUserApi] with ApiModuleProvi override def dataContainer: Option[String] = None override val name = "saveWorkflowUser" + val authz: List[AuthorizationType] = AuthorizationType.Administration.Write :: Nil } def endpoints = values.toList.sortBy(_.z) diff --git a/change-validation/src/test/scala/com/normation/plugins/changevalidation/ValidatedUserJdbcRepositoryTest.scala b/change-validation/src/test/scala/com/normation/plugins/changevalidation/ValidatedUserJdbcRepositoryTest.scala index 014555e30..8cef9698b 100644 --- a/change-validation/src/test/scala/com/normation/plugins/changevalidation/ValidatedUserJdbcRepositoryTest.scala +++ b/change-validation/src/test/scala/com/normation/plugins/changevalidation/ValidatedUserJdbcRepositoryTest.scala @@ -5,7 +5,7 @@ import better.files.Resource import cats.syntax.apply.* import com.normation.eventlog.EventActor import com.normation.rudder.db.DBCommon -import com.normation.rudder.rest.AuthorizationApiMapping +import com.normation.rudder.rest.ExtensibleAuthorizationApiMapping import com.normation.rudder.rest.RoleApiMapping import com.normation.rudder.users.FileUserDetailListProvider import com.normation.rudder.users.PasswordEncoderDispatcher @@ -71,7 +71,7 @@ class ValidatedUserJdbcRepositoryTest extends Specification with DBCommon with I val usersFile = { UserFile("test-users.xml", getUsersInputStream) } - val roleApiMapping = new RoleApiMapping(AuthorizationApiMapping.Core) + val roleApiMapping = new RoleApiMapping(new ExtensibleAuthorizationApiMapping(Nil)) val res = new FileUserDetailListProvider(roleApiMapping, usersFile, new PasswordEncoderDispatcher(12)) res.reload() diff --git a/datasources/src/main/scala/com/normation/plugins/datasources/api/DataSourceApi.scala b/datasources/src/main/scala/com/normation/plugins/datasources/api/DataSourceApi.scala index f15a3f9ce..edd851748 100644 --- a/datasources/src/main/scala/com/normation/plugins/datasources/api/DataSourceApi.scala +++ b/datasources/src/main/scala/com/normation/plugins/datasources/api/DataSourceApi.scala @@ -66,7 +66,8 @@ object DataSourceApi extends Enum[DataSourceApi] with ApiModuleProvider[Da val description = "Reload all datasources for all nodes" val (action, path) = POST / "datasources" / "reload" / "node" - override def dataContainer: Option[String] = None + override def dataContainer: Option[String] = None + val authz: List[AuthorizationType] = AuthorizationType.Administration.Write :: Nil } final case object ReloadAllDatasourcesOneNode extends DataSourceApi with OneParam with StartsAtVersion9 with SortIndex { @@ -74,7 +75,8 @@ object DataSourceApi extends Enum[DataSourceApi] with ApiModuleProvider[Da val description = "Reload all datasources for the given node" val (action, path) = POST / "datasources" / "reload" / "node" / "{nodeid}" - override def dataContainer: Option[String] = None + override def dataContainer: Option[String] = None + val authz: List[AuthorizationType] = AuthorizationType.Administration.Write :: Nil } final case object ReloadOneDatasourceAllNodes extends DataSourceApi with OneParam with StartsAtVersion9 with SortIndex { @@ -82,7 +84,8 @@ object DataSourceApi extends Enum[DataSourceApi] with ApiModuleProvider[Da val description = "Reload this given datasources for all nodes" val (action, path) = POST / "datasources" / "reload" / "{datasourceid}" - override def dataContainer: Option[String] = None + override def dataContainer: Option[String] = None + val authz: List[AuthorizationType] = AuthorizationType.Administration.Write :: Nil } final case object ReloadOneDatasourceOneNode extends DataSourceApi with TwoParam with StartsAtVersion9 with SortIndex { @@ -90,7 +93,8 @@ object DataSourceApi extends Enum[DataSourceApi] with ApiModuleProvider[Da val description = "Reload the given datasource for the given node" val (action, path) = POST / "datasources" / "reload" / "{datasourceid}" / "node" / "{nodeid}" - override def dataContainer: Option[String] = None + override def dataContainer: Option[String] = None + val authz: List[AuthorizationType] = AuthorizationType.Administration.Write :: Nil } final case object ClearValueOneDatasourceAllNodes extends DataSourceApi with OneParam with StartsAtVersion9 with SortIndex { @@ -98,7 +102,8 @@ object DataSourceApi extends Enum[DataSourceApi] with ApiModuleProvider[Da val description = "Clear node property values on all nodes for given datasource" val (action, path) = POST / "datasources" / "clear" / "{datasourceid}" - override def dataContainer: Option[String] = None + override def dataContainer: Option[String] = None + val authz: List[AuthorizationType] = AuthorizationType.Administration.Write :: Nil } final case object ClearValueOneDatasourceOneNode extends DataSourceApi with TwoParam with StartsAtVersion9 with SortIndex { @@ -106,7 +111,8 @@ object DataSourceApi extends Enum[DataSourceApi] with ApiModuleProvider[Da val description = "Clear node property value set by given datasource on given node" val (action, path) = POST / "datasources" / "clear" / "{datasourceid}" / "node" / "{nodeid}" - override def dataContainer: Option[String] = None + override def dataContainer: Option[String] = None + val authz: List[AuthorizationType] = AuthorizationType.Administration.Write :: Nil } final case object GetAllDataSources extends DataSourceApi with ZeroParam with StartsAtVersion9 with SortIndex { @@ -132,7 +138,8 @@ object DataSourceApi extends Enum[DataSourceApi] with ApiModuleProvider[Da val description = "Delete given datasource" val (action, path) = DELETE / "datasources" / "{datasourceid}" - override def dataContainer: Option[String] = Some("datasources") + override def dataContainer: Option[String] = Some("datasources") + val authz: List[AuthorizationType] = AuthorizationType.Administration.Write :: Nil } final case object CreateDataSource extends DataSourceApi with ZeroParam with StartsAtVersion9 with SortIndex { @@ -140,7 +147,8 @@ object DataSourceApi extends Enum[DataSourceApi] with ApiModuleProvider[Da val description = "Create given datasource" val (action, path) = PUT / "datasources" - override def dataContainer: Option[String] = Some("datasources") + override def dataContainer: Option[String] = Some("datasources") + val authz: List[AuthorizationType] = AuthorizationType.Administration.Write :: Nil } final case object UpdateDataSource extends DataSourceApi with OneParam with StartsAtVersion9 with SortIndex { @@ -148,7 +156,8 @@ object DataSourceApi extends Enum[DataSourceApi] with ApiModuleProvider[Da val description = "Update information about the given datasource" val (action, path) = POST / "datasources" / "{datasourceid}" - override def dataContainer: Option[String] = Some("datasources") + override def dataContainer: Option[String] = Some("datasources") + val authz: List[AuthorizationType] = AuthorizationType.Administration.Write :: Nil } def endpoints = values.toList.sortBy(_.z) diff --git a/scale-out-relay/src/main/scala/com/normation/plugins/scaleoutrelay/api/ScaleOutRelayApi.scala b/scale-out-relay/src/main/scala/com/normation/plugins/scaleoutrelay/api/ScaleOutRelayApi.scala index 33654b073..ecd6383f2 100644 --- a/scale-out-relay/src/main/scala/com/normation/plugins/scaleoutrelay/api/ScaleOutRelayApi.scala +++ b/scale-out-relay/src/main/scala/com/normation/plugins/scaleoutrelay/api/ScaleOutRelayApi.scala @@ -3,6 +3,7 @@ package com.normation.plugins.scaleoutrelay.api import com.normation.eventlog.ModificationId import com.normation.inventory.domain.NodeId import com.normation.plugins.scaleoutrelay.ScaleOutRelayService +import com.normation.rudder.AuthorizationType import com.normation.rudder.api.ApiVersion import com.normation.rudder.api.HttpAction.POST import com.normation.rudder.facts.nodes.ChangeContext @@ -29,12 +30,14 @@ object ScaleOutRelayApi extends Enum[ScaleOutRelayApi] with ApiModuleProvider[Sc val z = implicitly[Line].value val description = "Promote a node to relay" val (action, path) = POST / "scaleoutrelay" / "promote" / "{nodeId}" + val authz: List[AuthorizationType] = AuthorizationType.Administration.Write :: Nil } final case object DemoteToNode extends ScaleOutRelayApi with OneParam with StartsAtVersion14 { val z = implicitly[Line].value val description = "Demote a relay to a simple node" val (action, path) = POST / "scaleoutrelay" / "demote" / "{nodeId}" + val authz: List[AuthorizationType] = AuthorizationType.Administration.Write :: Nil } override def endpoints: List[ScaleOutRelayApi] = values.toList.sortBy(_.z) From c20a014be5246379710f6c87abd85e988e4df4c1 Mon Sep 17 00:00:00 2001 From: "Francois @fanf42 Armand" Date: Fri, 28 Feb 2025 19:16:52 +0100 Subject: [PATCH 25/65] Fixes #26457: Impact of #26335 to ApiAuthorization - again --- .../UserTokenApiDefinition.scala | 56 +++++++------------ .../apiauthorizations/MockServices.scala | 4 +- .../apiauthorizations/api/UserApiTest.scala | 14 +---- .../rudder/plugin/AuthBackendsConf.scala | 4 +- .../snippet/Oauth2LoginBanner.scala | 2 - .../rudder/plugin/BrandingPluginConf.scala | 2 +- .../branding/snippet/LoginBranding.scala | 3 +- 7 files changed, 29 insertions(+), 56 deletions(-) diff --git a/api-authorizations/src/main/scala/com/normation/plugins/apiauthorizations/UserTokenApiDefinition.scala b/api-authorizations/src/main/scala/com/normation/plugins/apiauthorizations/UserTokenApiDefinition.scala index ed57baaef..51ea514d5 100644 --- a/api-authorizations/src/main/scala/com/normation/plugins/apiauthorizations/UserTokenApiDefinition.scala +++ b/api-authorizations/src/main/scala/com/normation/plugins/apiauthorizations/UserTokenApiDefinition.scala @@ -38,27 +38,23 @@ package com.normation.plugins.apiauthorizations import bootstrap.liftweb.AuthBackendProvidersManager - import com.normation.errors.* import com.normation.eventlog.ModificationId import com.normation.rudder.api.* -import com.normation.rudder.apidata.JsonApiAcl import com.normation.rudder.facts.nodes.NodeSecurityContext import com.normation.rudder.rest.* import com.normation.rudder.rest.UserApi -import com.normation.rudder.rest.data.ApiAccountCodec +import com.normation.rudder.rest.data.* import com.normation.rudder.rest.implicits.ToLiftResponseOne import com.normation.rudder.rest.lift.* import com.normation.rudder.users.UserRepository import com.normation.utils.DateFormaterService import com.normation.utils.StringUuidGenerator - import io.scalaland.chimney.Transformer import io.scalaland.chimney.syntax.* import net.liftweb.http.LiftResponse import net.liftweb.http.Req import org.joda.time.DateTime - import zio.json.* class UserApiImpl( @@ -76,17 +72,13 @@ class UserApiImpl( def schemas: ApiModuleProvider[UserApi] = UserApi def getLiftEndpoints(): List[LiftApiModule] = { - UserApi.endpoints - .map(e => { - e match { - case UserApi.GetTokenFeatureStatus => GetTokenFeatureStatus - case UserApi.GetApiToken => GetApiToken - case UserApi.CreateApiToken => CreateApiToken - case UserApi.DeleteApiToken => DeleteApiToken - case UserApi.UpdateApiToken => UpdateApiToken - } - }) - .toList + UserApi.endpoints.map { + case UserApi.GetTokenFeatureStatus => GetTokenFeatureStatus + case UserApi.GetApiToken => GetApiToken + case UserApi.CreateApiToken => CreateApiToken + case UserApi.DeleteApiToken => DeleteApiToken + case UserApi.UpdateApiToken => UpdateApiToken + } } /* @@ -133,13 +125,13 @@ class UserApiImpl( def process0(version: ApiVersion, path: ApiPath, req: Req, params: DefaultParams, authzToken: AuthzToken): LiftResponse = { val now = DateTime.now - val secret = ApiToken.generate_secret(tokenGenerator) - val hash = ApiToken.hash(secret) + val secret = ApiTokenSecret.generate(tokenGenerator) + val hash = ApiTokenHash.fromSecret(secret) val account = ApiAccount( ApiAccountId(authzToken.qc.actor.name), ApiAccountKind.User, ApiAccountName(authzToken.qc.actor.name), - Some(ApiToken(hash)), + Some(hash), s"API token for user '${authzToken.qc.actor.name}'", isEnabled = true, now, @@ -150,7 +142,7 @@ class UserApiImpl( writeApi .save(account, ModificationId(uuidGen.newUuid), authzToken.qc.actor) - .map(RestAccountsResponse.fromUnredacted(_, secret)) + .map(RestAccountsResponse.fromUnredacted(_, secret.exposeSecret())) .chainError(s"Error when trying to save user '${authzToken.qc.actor.name}' API token") .toLiftResponseOne(params, schema, None) } @@ -185,17 +177,12 @@ class UserApiImpl( object UserApiImpl { /** - * The value that will be displayed in the API response for the token. - */ + * The value that will be displayed in the API response for the token. + */ final case class ClearTextToken(value: String) extends AnyVal object ClearTextToken { - implicit val transformer: Transformer[ApiToken, ClearTextToken] = Transformer - .define[ApiToken, ClearTextToken] - .withFieldComputed(_.value, token => if (token.isHashed) "" else token.value) - .buildTransformer - implicit val encoder: JsonEncoder[ClearTextToken] = JsonEncoder[String].contramap(_.value) } @@ -211,10 +198,10 @@ object UserApiImpl { expirationDate: Option[String], expirationDateDefined: Boolean, authorizationType: Option[ApiAuthorizationKind], - acl: Option[List[JsonApiAcl]] + acl: Option[List[JsonApiPerm]] ) - object RestApiAccount extends ApiAccountCodec { + object RestApiAccount extends ApiAccountCodecs { implicit class ApiAccountOps(val account: ApiAccount) extends AnyVal { import ApiAccountKind.* def expirationDate: Option[String] = { @@ -233,13 +220,13 @@ object UserApiImpl { } } - def acl: Option[List[JsonApiAcl]] = { + def acl: Option[List[JsonApiPerm]] = { import ApiAuthorization.* account.kind match { case PublicApi(authz, expirationDate) => authz match { case None | RO | RW => Option.empty - case ACL(acls) => Some(acls.flatMap(x => x.actions.map(a => JsonApiAcl(x.path.value, a.name)))) + case ACL(acls) => Some(acls.flatMap(x => x.actions.map(a => JsonApiPerm(x.path.value, a.name)))) } case User | System => Option.empty } @@ -258,7 +245,7 @@ object UserApiImpl { implicit val transformer: Transformer[ApiAccount, RestApiAccount] = Transformer .define[ApiAccount, RestApiAccount] - .withFieldComputedFrom(_.token)(_.token, _.fold(ClearTextToken(""))(_.transformInto[ClearTextToken])) + .withFieldConst(_.token, ClearTextToken("")) // if the hash need to be exposed, it's done post transformation .withFieldComputed(_.kind, _.kind.kind) .withFieldComputed(_.acl, _.acl) .withFieldComputed(_.expirationDate, _.expirationDate) @@ -269,9 +256,6 @@ object UserApiImpl { ) .buildTransformer - implicit val publicTokenEncoder: JsonEncoder[ApiToken] = - JsonEncoder[String].contramap(_.value) - implicit val encoder: JsonEncoder[RestApiAccount] = DeriveJsonEncoder.gen[RestApiAccount] } @@ -317,7 +301,7 @@ object UserApiImpl { accounts: RestAccountId ) - object RestAccountIdResponse extends ApiAccountCodec { + object RestAccountIdResponse extends ApiAccountCodecs { implicit val accountIdResponseEncoder: JsonEncoder[RestAccountId] = DeriveJsonEncoder.gen[RestAccountId] implicit val encoder: JsonEncoder[RestAccountIdResponse] = DeriveJsonEncoder.gen[RestAccountIdResponse] diff --git a/api-authorizations/src/test/scala/com/normation/plugins/apiauthorizations/MockServices.scala b/api-authorizations/src/test/scala/com/normation/plugins/apiauthorizations/MockServices.scala index 3446fc934..fed3736e1 100644 --- a/api-authorizations/src/test/scala/com/normation/plugins/apiauthorizations/MockServices.scala +++ b/api-authorizations/src/test/scala/com/normation/plugins/apiauthorizations/MockServices.scala @@ -5,7 +5,7 @@ import com.normation.eventlog.EventActor import com.normation.eventlog.ModificationId import com.normation.rudder.api.ApiAccount import com.normation.rudder.api.ApiAccountId -import com.normation.rudder.api.ApiToken +import com.normation.rudder.api.ApiTokenHash import com.normation.rudder.api.RoApiAccountRepository import com.normation.rudder.api.TokenGenerator import com.normation.rudder.api.WoApiAccountRepository @@ -27,7 +27,7 @@ class MockServices(newToken: String, accounts: Map[ApiAccountId, ApiAccount] = M } override def getAllStandardAccounts: IOResult[Seq[ApiAccount]] = ??? - override def getByToken(token: ApiToken): IOResult[Option[ApiAccount]] = ??? + override def getByToken(token: ApiTokenHash): IOResult[Option[ApiAccount]] = ??? override def getSystemAccount: ApiAccount = ??? } diff --git a/api-authorizations/src/test/scala/com/normation/plugins/apiauthorizations/api/UserApiTest.scala b/api-authorizations/src/test/scala/com/normation/plugins/apiauthorizations/api/UserApiTest.scala index b3e06f266..234cc5ce5 100644 --- a/api-authorizations/src/test/scala/com/normation/plugins/apiauthorizations/api/UserApiTest.scala +++ b/api-authorizations/src/test/scala/com/normation/plugins/apiauthorizations/api/UserApiTest.scala @@ -4,19 +4,11 @@ import better.files.* import com.normation.errors.IOResult import com.normation.errors.effectUioUnit import com.normation.rudder.AuthorizationType -import com.normation.rudder.api.ApiAccount -import com.normation.rudder.api.ApiAccountId -import com.normation.rudder.api.ApiAccountKind -import com.normation.rudder.api.ApiAccountName -import com.normation.rudder.api.ApiAuthorization -import com.normation.rudder.api.ApiToken -import com.normation.rudder.api.ApiVersion +import com.normation.rudder.api.* import com.normation.rudder.facts.nodes.NodeSecurityContext import com.normation.rudder.rest.RestTestSetUp import com.normation.rudder.rest.TraitTestApiFromYamlFiles -import com.normation.rudder.users.AuthenticatedUser -import com.normation.rudder.users.RudderAccount -import com.normation.rudder.users.UserService +import com.normation.rudder.users.* import java.nio.file.Files import org.joda.time.DateTime import org.joda.time.DateTimeUtils @@ -43,7 +35,7 @@ class UserApiTest extends ZIOSpecDefault { ApiAccountId("user1"), ApiAccountKind.System, // so that we have access to the plugin endpoints ApiAccountName("user1"), - Some(ApiToken("v2:some-hashed-token")), + Some(ApiTokenHash.fromHashValue("v2:some-hashed-token")), "number one user", isEnabled = true, creationDate = accountCreationDate, diff --git a/auth-backends/src/main/scala/bootstrap/rudder/plugin/AuthBackendsConf.scala b/auth-backends/src/main/scala/bootstrap/rudder/plugin/AuthBackendsConf.scala index 6601dee97..836d78868 100644 --- a/auth-backends/src/main/scala/bootstrap/rudder/plugin/AuthBackendsConf.scala +++ b/auth-backends/src/main/scala/bootstrap/rudder/plugin/AuthBackendsConf.scala @@ -221,7 +221,7 @@ object AuthBackendsConf extends RudderPluginModule { if (isOauthConfiguredByUser) { PluginLogger.info(s"Oauthv2 or OIDC authentication backend is enabled, updating login form") RudderConfig.snippetExtensionRegister.register( - new Oauth2LoginBanner(pluginStatusService, pluginDef.version, oauth2registrations) + new Oauth2LoginBanner(pluginStatusService, oauth2registrations) ) } } @@ -1331,7 +1331,7 @@ class RudderJwtAuthenticationConverter( ApiAccountId(jwt.getId), ApiAccountKind.PublicApi(apiAuthz, exp), ApiAccountName(jwt.getId), - Some(ApiToken(jwt.getTokenValue)), + Some(ApiTokenHash.fromHashValue(jwt.getTokenValue)), "", isEnabled = true, // always enabled at that point, since the token is valid created, diff --git a/auth-backends/src/main/scala/com/normation/plugins/authbackends/snippet/Oauth2LoginBanner.scala b/auth-backends/src/main/scala/com/normation/plugins/authbackends/snippet/Oauth2LoginBanner.scala index be0f6015e..b23cb89cf 100644 --- a/auth-backends/src/main/scala/com/normation/plugins/authbackends/snippet/Oauth2LoginBanner.scala +++ b/auth-backends/src/main/scala/com/normation/plugins/authbackends/snippet/Oauth2LoginBanner.scala @@ -40,7 +40,6 @@ package com.normation.plugins.authbackends.snippet import bootstrap.rudder.plugin.AuthBackendsConf import com.normation.plugins.PluginExtensionPoint import com.normation.plugins.PluginStatus -import com.normation.plugins.PluginVersion import com.normation.plugins.authbackends.LoginFormRendering import com.normation.plugins.authbackends.RudderPropertyBasedOAuth2RegistrationDefinition import com.normation.rudder.web.snippet.Login @@ -53,7 +52,6 @@ import scala.xml.NodeSeq class Oauth2LoginBanner( val status: PluginStatus, - version: PluginVersion, registrations: RudderPropertyBasedOAuth2RegistrationDefinition )(implicit val ttag: ClassTag[Login]) extends PluginExtensionPoint[Login] { diff --git a/branding/src/main/scala/bootstrap/rudder/plugin/BrandingPluginConf.scala b/branding/src/main/scala/bootstrap/rudder/plugin/BrandingPluginConf.scala index 4afed2110..e0114d39e 100644 --- a/branding/src/main/scala/bootstrap/rudder/plugin/BrandingPluginConf.scala +++ b/branding/src/main/scala/bootstrap/rudder/plugin/BrandingPluginConf.scala @@ -65,5 +65,5 @@ object BrandingPluginConf extends RudderPluginModule { RudderConfig.rudderApi.addModules(brandingApi.getLiftEndpoints()) RudderConfig.snippetExtensionRegister.register(new BrandingResources(pluginDef.status)) RudderConfig.snippetExtensionRegister.register(new CommonBranding(pluginStatusService)) - RudderConfig.snippetExtensionRegister.register(new LoginBranding(pluginStatusService, pluginDef.version)) + RudderConfig.snippetExtensionRegister.register(new LoginBranding(pluginStatusService)) } diff --git a/branding/src/main/scala/com/normation/plugins/branding/snippet/LoginBranding.scala b/branding/src/main/scala/com/normation/plugins/branding/snippet/LoginBranding.scala index f7b3a2243..ad472602d 100644 --- a/branding/src/main/scala/com/normation/plugins/branding/snippet/LoginBranding.scala +++ b/branding/src/main/scala/com/normation/plugins/branding/snippet/LoginBranding.scala @@ -40,7 +40,6 @@ package com.normation.plugins.branding.snippet import bootstrap.rudder.plugin.BrandingPluginConf import com.normation.plugins.PluginExtensionPoint import com.normation.plugins.PluginStatus -import com.normation.plugins.PluginVersion import com.normation.rudder.web.snippet.Login import com.normation.zio.UnsafeRun import net.liftweb.common.Loggable @@ -48,7 +47,7 @@ import net.liftweb.util.Helpers.* import scala.reflect.ClassTag import scala.xml.NodeSeq -class LoginBranding(val status: PluginStatus, version: PluginVersion)(implicit val ttag: ClassTag[Login]) +class LoginBranding(val status: PluginStatus)(implicit val ttag: ClassTag[Login]) extends PluginExtensionPoint[Login] with Loggable { def pluginCompose(snippet: Login): Map[String, NodeSeq => NodeSeq] = Map( From 7e83e3569a9bc4931703c7abe156068fa1098a3a Mon Sep 17 00:00:00 2001 From: "Francois @fanf42 Armand" Date: Mon, 3 Mar 2025 16:55:17 +0100 Subject: [PATCH 26/65] Fixes #26460: Impact of Scala 3 - 26459 - public plugins --- .../changevalidation/ChangeRequestJson.scala | 2 +- .../plugins/changevalidation/DataTypes.scala | 5 ++--- .../snippet/ChangeRequestDetails.scala | 2 +- .../ChangeRequestJdbcRepositoryTest.scala | 3 ++- .../plugins/changevalidation/TestEmailService.scala | 13 +++++++------ .../plugins/datasources/UpdateHttpDatasetTest.scala | 1 - plugins-common/pom-template.xml | 1 - .../scaleoutrelay/ScaleOutRelayService.scala | 10 +++++----- 8 files changed, 18 insertions(+), 19 deletions(-) diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJson.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJson.scala index 35fbfa572..1e565d3c9 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJson.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJson.scala @@ -355,7 +355,7 @@ object DirectiveChangeJson { technique: Technique, diffService: DiffService ): PartialTransformer[ModifyToDirectiveDiff, DirectiveModifyChangeJson] = { - case (ModifyToDirectiveDiff(techniqueName, directive, rootSection), _) => + case (ModifyToDirectiveDiff(_, directive, rootSection), _) => val result = change.initialState match { case Some((techniqueName, initialState, section @ Some(initialRootSection))) => val diff = diffService.diffDirective(initialState, section, directive, rootSection, techniqueName) diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/DataTypes.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/DataTypes.scala index f335e5200..0708ab128 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/DataTypes.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/DataTypes.scala @@ -50,7 +50,6 @@ import com.normation.rudder.repository.FullNodeGroupCategory import io.scalaland.chimney.Transformer import net.liftweb.common.Logger import org.slf4j.LoggerFactory -import scala.annotation.nowarn import scala.collection.immutable.SortedSet import zio.NonEmptyChunk import zio.json.* @@ -167,7 +166,7 @@ trait TargetJsonCodec { implicit val unsupervisedTargetIdsEncoder: JsonEncoder[UnsupervisedTargetIds] = DeriveJsonEncoder.gen[UnsupervisedTargetIds] implicit val unsupervisedTargetIdsDecoder: JsonDecoder[UnsupervisedTargetIds] = { - @nowarn implicit val simpleTargetDecoder: JsonDecoder[SimpleTarget] = JsonDecoder[String].mapOrFail(s => { + implicit val simpleTargetDecoder: JsonDecoder[SimpleTarget] = JsonDecoder[String].mapOrFail(s => { RuleTarget .unser(s) .collect { case t: SimpleTarget => t } @@ -177,7 +176,7 @@ trait TargetJsonCodec { } implicit val supervisedSimpleTargetsDecoder: JsonDecoder[SupervisedSimpleTargets] = { - @nowarn implicit val loggedSimpleTargetDecoder: JsonDecoder[SimpleTarget] = { + implicit val loggedSimpleTargetDecoder: JsonDecoder[SimpleTarget] = { JsonDecoder[String].mapOrFail(s => { RuleTarget .unser(s) diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestDetails.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestDetails.scala index 94120aa9c..965893219 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestDetails.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestDetails.scala @@ -341,7 +341,7 @@ class ChangeRequestDetails extends DispatchSnippet with Loggable { case Mandatory => Some(buildReasonField(true, "subContainerReasonField")) case Optional => Some(buildReasonField(false, "subContainerReasonField")) // for non-exhaustiveness God - yes, enum were not very well designed before scala 3 - case x => throw new IllegalArgumentException(s"This case should not happen, please report to developers") + case _ => throw new IllegalArgumentException(s"This case should not happen, please report to developers") } } diff --git a/change-validation/src/test/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepositoryTest.scala b/change-validation/src/test/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepositoryTest.scala index 525d3bd94..8b40ef990 100644 --- a/change-validation/src/test/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepositoryTest.scala +++ b/change-validation/src/test/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepositoryTest.scala @@ -309,7 +309,8 @@ class ChangeRequestJdbcRepositoryTest extends Specification with DBCommon with I "get change request by filter" in { val res = roChangeRequestJdbcRepository.getByFilter(ChangeRequestFilter(None, None)).runNow - (res.size must beEqualTo(1)) and (res.head must beEqualTo((expectedChangeRequest, WorkflowNodeId("Pending validation")))) + res.size must beEqualTo(1) + res.head must beEqualTo((expectedChangeRequest, WorkflowNodeId("Pending validation"))) } "create change request" in { diff --git a/change-validation/src/test/scala/com/normation/plugins/changevalidation/TestEmailService.scala b/change-validation/src/test/scala/com/normation/plugins/changevalidation/TestEmailService.scala index 06c9a9063..e9fa7860b 100644 --- a/change-validation/src/test/scala/com/normation/plugins/changevalidation/TestEmailService.scala +++ b/change-validation/src/test/scala/com/normation/plugins/changevalidation/TestEmailService.scala @@ -106,7 +106,8 @@ class TestEmailService extends Specification with BeforeAfterAll { val config = notification.getSMTPConf(conf.pathAsString).forceGet - (config.smtpHostServer === "localhost") and (config.port === 2525) + config.smtpHostServer === "localhost" + config.port === 2525 // todo more tests } @@ -114,8 +115,8 @@ class TestEmailService extends Specification with BeforeAfterAll { val template = notification.getStepMailConf(TwoValidationStepsWorkflowServiceImpl.Validation, conf.pathAsString).forceGet - (template.to === Set(Email("validator1@change.req"), Email("validator2@change.req")) and - (template.template === (testDir / "validation-mail.template").pathAsString)) + template.to === Set(Email("validator1@change.req"), Email("validator2@change.req")) + template.template === (testDir / "validation-mail.template").pathAsString } "be able to send a notification email" in { @@ -166,9 +167,9 @@ class TestEmailService extends Specification with BeforeAfterAll { s.split("\n").take(2).drop(3).take(2).drop(1).take(20).mkString("\n") } - (isEmpty must beTrue) and - (messages.isEmpty must beFalse) and - (deleteDate(msg) === deleteDate(expectedMessage)) + isEmpty must beTrue + messages.isEmpty must beFalse + deleteDate(msg) === deleteDate(expectedMessage) } } } diff --git a/datasources/src/test/scala/com/normation/plugins/datasources/UpdateHttpDatasetTest.scala b/datasources/src/test/scala/com/normation/plugins/datasources/UpdateHttpDatasetTest.scala index 180ad5275..ebe96b993 100644 --- a/datasources/src/test/scala/com/normation/plugins/datasources/UpdateHttpDatasetTest.scala +++ b/datasources/src/test/scala/com/normation/plugins/datasources/UpdateHttpDatasetTest.scala @@ -150,7 +150,6 @@ object TestingZioHttpServer { /** * This is just a test program to see how test clock works */ -@nowarn("msg=a type was inferred to be `\\w+`; this may indicate a programming error.") object TestingSpacedClock { val makeTestClock = TestClock.default.build diff --git a/plugins-common/pom-template.xml b/plugins-common/pom-template.xml index 0d9771bc5..bb005dfdc 100644 --- a/plugins-common/pom-template.xml +++ b/plugins-common/pom-template.xml @@ -256,7 +256,6 @@ co.fs2fs2-io_${scala-binary-version}provided com.beachapeenumeratum_${scala-binary-version}provided com.beachapeenumeratum-macros_${scala-binary-version}provided - com.chuusaishapeless_${scala-binary-version}provided${shapeless-version} com.comcastip4s-core_${scala-binary-version}provided com.github.alonsodomin.cron4scron4s-core_${scala-binary-version}provided${cron4s-version} com.github.ben-manes.caffeinecaffeineprovided${caffeine-version} diff --git a/scale-out-relay/src/main/scala/com/normation/plugins/scaleoutrelay/ScaleOutRelayService.scala b/scale-out-relay/src/main/scala/com/normation/plugins/scaleoutrelay/ScaleOutRelayService.scala index ac3290281..76f18be55 100644 --- a/scale-out-relay/src/main/scala/com/normation/plugins/scaleoutrelay/ScaleOutRelayService.scala +++ b/scale-out-relay/src/main/scala/com/normation/plugins/scaleoutrelay/ScaleOutRelayService.scala @@ -113,9 +113,9 @@ class ScaleOutRelayService( val msg = s"Promote node ${nodeInfo.id.value} have failed. Change were reverted. Cause was: ${err.fullMsg}" (for { _ <- ScaleOutRelayLoggerPure.debug(s"[promote ${nodeInfo.id.value}] error, start reverting") - _ <- demoteRelay(nodeInfo, objects).catchAll(err => - ScaleOutRelayLoggerPure.debug(s"Error when reverting node promotion: ${err.fullMsg}") - ) + _ <- demoteRelay(nodeInfo, objects) + .tapError(err => ScaleOutRelayLoggerPure.debug(s"Error when reverting node promotion: ${err.fullMsg}")) + .ignore _ <- ScaleOutRelayLoggerPure.error(msg) } yield {}) *> Unexpected(msg).fail } @@ -174,10 +174,10 @@ class ScaleOutRelayService( val rules = objects.rules.map(r => { woRuleRepository .deleteSystemRule(r.id, cc.modId, cc.actor, cc.message) - .catchAll { err => + .tapError { err => ScaleOutRelayLoggerPure.info(s"Trying to remove residual object rule ${r.id.serialize}: ${err.fullMsg}") } - .unit + .ignore }) (nPromoted :: nBeforePromoted :: targets ::: groups ::: directives ::: rules).accumulate(identity) From 260348b4a40e4158663d22a98b1fbaed3f384898 Mon Sep 17 00:00:00 2001 From: Fdall Date: Wed, 5 Mar 2025 12:10:03 +0100 Subject: [PATCH 27/65] Fixes #26474: Rename the field requires_license to requires-license --- api-authorizations/packaging/metadata | 2 +- auth-backends/packaging/metadata | 2 +- change-validation/packaging/metadata | 2 +- datasources/packaging/metadata | 2 +- scale-out-relay/packaging/metadata | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api-authorizations/packaging/metadata b/api-authorizations/packaging/metadata index 4b07df00b..b3003cd9a 100644 --- a/api-authorizations/packaging/metadata +++ b/api-authorizations/packaging/metadata @@ -9,5 +9,5 @@ "content": { "files.txz": "/opt/rudder/share/plugins" }, - "requires_license": true + "requires-license": true } diff --git a/auth-backends/packaging/metadata b/auth-backends/packaging/metadata index c91eb08b1..ba4bfabd8 100644 --- a/auth-backends/packaging/metadata +++ b/auth-backends/packaging/metadata @@ -9,5 +9,5 @@ "content": { "files.txz": "/opt/rudder/share/plugins" }, - "requires_license": true + "requires-license": true } diff --git a/change-validation/packaging/metadata b/change-validation/packaging/metadata index 94d289e95..5a1309939 100644 --- a/change-validation/packaging/metadata +++ b/change-validation/packaging/metadata @@ -9,5 +9,5 @@ "content": { "files.txz": "/opt/rudder/share/plugins" }, - "requires_license": true + "requires-license": true } diff --git a/datasources/packaging/metadata b/datasources/packaging/metadata index 30fad468d..27ac0e312 100644 --- a/datasources/packaging/metadata +++ b/datasources/packaging/metadata @@ -9,5 +9,5 @@ "content": { "files.txz": "/opt/rudder/share/plugins" }, - "requires_license": true + "requires-license": true } diff --git a/scale-out-relay/packaging/metadata b/scale-out-relay/packaging/metadata index d535b597c..611128789 100644 --- a/scale-out-relay/packaging/metadata +++ b/scale-out-relay/packaging/metadata @@ -9,5 +9,5 @@ "content": { "files.txz": "/opt/rudder/share/plugins" }, - "requires_license": true + "requires-license": true } From bfab6c64a7147bca750ef96b18e504226138cf48 Mon Sep 17 00:00:00 2001 From: Alexis Mousset Date: Wed, 5 Mar 2025 14:23:53 +0100 Subject: [PATCH 28/65] Fixes #26477: Add missing licenses --- branding/packaging/metadata | 3 ++- node-external-reports/packaging/metadata | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/branding/packaging/metadata b/branding/packaging/metadata index 096eac652..20520a4d4 100644 --- a/branding/packaging/metadata +++ b/branding/packaging/metadata @@ -8,5 +8,6 @@ "jar-files": [ "/opt/rudder/share/plugins/${plugin-name}/${plugin-name}.jar" ], "content": { "files.txz": "/opt/rudder/share/plugins" - } + }, + "requires-license": true } diff --git a/node-external-reports/packaging/metadata b/node-external-reports/packaging/metadata index d3d0c4d99..915e1b34a 100644 --- a/node-external-reports/packaging/metadata +++ b/node-external-reports/packaging/metadata @@ -8,5 +8,6 @@ "jar-files": [ "/opt/rudder/share/plugins/${plugin-name}/${plugin-name}.jar" ], "content": { "files.txz": "/opt/rudder/share/plugins" - } + }, + "requires-license": true } From 4a9b9fcec80073d884a7c6d9aeb2d547218d04a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Membr=C3=A9?= Date: Thu, 6 Mar 2025 11:14:55 +0100 Subject: [PATCH 29/65] Prepare next nightly branch of plugin --- main-build.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main-build.conf b/main-build.conf index 00cb85169..0de2e4e47 100644 --- a/main-build.conf +++ b/main-build.conf @@ -14,6 +14,6 @@ # Version of Rudder used to build the plugin. # It defined the API/ABI used and it is important for binary compatibility branch-type=next -rudder-version=8.3.0~alpha2 +rudder-version=8.3.0~beta1 common-version=2.1.1 private-version=2.1.0 From a8217ea4c577292ddac4bbdb0e6eb70e32f95535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Membr=C3=A9?= Date: Thu, 6 Mar 2025 11:15:49 +0100 Subject: [PATCH 30/65] Prepare next nightly branch of plugin --- main-build.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main-build.conf b/main-build.conf index 0de2e4e47..167b1fef6 100644 --- a/main-build.conf +++ b/main-build.conf @@ -14,6 +14,6 @@ # Version of Rudder used to build the plugin. # It defined the API/ABI used and it is important for binary compatibility branch-type=next -rudder-version=8.3.0~beta1 +rudder-version=8.3.0~beta2 common-version=2.1.1 private-version=2.1.0 From 8b22af99f9fd843651b34dfd3e702faf53b47a8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Membr=C3=A9?= Date: Mon, 17 Mar 2025 11:59:01 +0100 Subject: [PATCH 31/65] Prepare next branch of plugin --- main-build.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main-build.conf b/main-build.conf index a7812a470..b5dc454e0 100644 --- a/main-build.conf +++ b/main-build.conf @@ -14,6 +14,6 @@ # Version of Rudder used to build the plugin. # It defined the API/ABI used and it is important for binary compatibility branch-type=next -rudder-version=9.0.0~alpha1 +rudder-version=9.1.0~alpha1 common-version=2.1.1 private-version=2.1.0 From 3e0f00c6f2e2d576b81ec2db9ec96531ec4021a0 Mon Sep 17 00:00:00 2001 From: Raphael Gauthier Date: Thu, 20 Mar 2025 16:57:40 +0100 Subject: [PATCH 32/65] Fixes #26583: Navbar menu is broken --- .../template/ChangeValidationManagement.html | 609 +++++++++--------- .../snippet/ChangeValidationSettings.scala | 19 +- 2 files changed, 305 insertions(+), 323 deletions(-) diff --git a/change-validation/src/main/resources/template/ChangeValidationManagement.html b/change-validation/src/main/resources/template/ChangeValidationManagement.html index 6363db0eb..44577c3c1 100644 --- a/change-validation/src/main/resources/template/ChangeValidationManagement.html +++ b/change-validation/src/main/resources/template/ChangeValidationManagement.html @@ -1,320 +1,313 @@ - - - - - - + + + + + + -
    +
    -
    - -
    - - - -
    -
    -
    -
    -

    Change validation status

    -
    -
    -
    -
      -
    • -
      - - -
      -
    • -
    • -
      - - -
      -
    • -
    • -
      - - -
      -
    • -
    - - - [messages] - -
    -
    -
    -
    -
    - + -
    -
    -
    -

    Configure email notification

    -
    -
    -

    You can modify the email's template of each steps here:

    -
      -
    • - /var/rudder/plugins/change-validation/validation-mail.template - - - -
    • -
    • - /var/rudder/plugins/change-validation/deployment-mail.template - - - -
    • -
    • - /var/rudder/plugins/change-validation/cancelled-mail.template - - - -
    • -
    • - /var/rudder/plugins/change-validation/deployed-mail.template - - - -
    • -
    -
    -
    -
    -
    - + +
    +
    +
    +
    +

    Change validation status

    +
    +
    +
    +
      +
    • +
      + + +
      +
    • +
    • +
      + + +
      +
    • +
    • +
      + + +
      +
    • +
    + + + [messages] + +
    +
    +
    +
    +
    + +
    +

    + If enabled, all change to configuration (directives, rules, groups and parameters) will be + submitted for validation via a change request based on node targeting (configured + below).
    + A new change request will enter the Pending validation status, then can be moved to + Pending deployment (approved but not yet deployed) or Deployed (approved and + deployed) statuses. +

    +

    + If you have the user management plugin, only users with the validator or + deployer roles are authorized to perform + these steps (see /opt/rudder/etc/rudder-users.xml). +

    +

    + If disabled or if the change is not submitted to validation, the configuration will be + immediately deployed. +

    +
    +
    +
    +
    +
    +

    Configure email notification

    +
    +
    +

    You can modify the email's template of each steps here:

    +
      +
    • + /var/rudder/plugins/change-validation/validation-mail.template + + + +
    • +
    • + /var/rudder/plugins/change-validation/deployment-mail.template + + + +
    • +
    • + /var/rudder/plugins/change-validation/cancelled-mail.template + + + +
    • +
    • + /var/rudder/plugins/change-validation/deployed-mail.template + + + +
    • +
    +
    +
    +
    +
    + +
    +

    + By default, email notifications are disabled. To enable them, make sure that the + smtp.hostServer parameter is + not left empty in the configuration file: + /opt/rudder/etc/plugins/change-validation.conf +

    +
    +
    +
    +
    +
    +

    Configure change request triggers

    +
    +

    + By default, change request are created for all users. You can change when a change request + is created with below options: +

    +
      +
    • exempt some users from validation;
    • +
    • trigger change request only for changes impacting nodes belonging to some supervised groups; +
    • +
    +

    Be careful: a change request is created when at least one predicate matches, so an + exempted user + still need a change request to modify a node from a supervised group. +

    +
    +

    Configure users with change validation

    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +

    + Any change done by a validated user will be automatically deployed without validation needed + by another user. +

    +
    +
    +
    + +
    +
    +
    +
    +
    +
      +
    • +
      + + +
      +
    • +
    + + [messages] +
    +
    +
    +
    +
    + +
    +

    + Any change done by a Validated User will be automatically approved no matter the nature of the change. +

    +

    + Configuring groups below will hence have no effect on validated users (in the list above), but will apply to non-validated users, who will still need a change request to modify a node from a supervised group. +

    +
    +
    +
    + +
    +
    +
    +

    Configure groups with change validations

    +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +

    + Change validation are enable for any change that would impact a node belonging to one + of the chosen groups below. + Be careful: a change on one another group +

    +

    + The supervised changes are: +

      +
    • any change in a global parameter, as these changes can have side effects spreading + technique code,
    • +
    • any modification in one of the supervised groups,
    • +
    • any change in a rule which targets a node which belong to a group marked as supervised, +
    • +
    • any change in a directive used in one of the previous rules.
    • +
    +

    +

    + Changes in techniques are not subjected to change validation, nor are changes resulting from + an archive import. +

    +
    +
    +
    +
    +
    +
    -

    - By default, email notifications are disabled. To enable them, make sure that the - smtp.hostServer parameter is - not left empty in the configuration file: - /opt/rudder/etc/plugins/change-validation.conf -

    -
    -
    -
    -

    Configure change request triggers

    -
    -

    - By default, change request are created for all users. You can change when a change request - is created with below options: -

    -
      -
    • exempt some users from validation;
    • -
    • trigger change request only for changes impacting nodes belonging to some supervised groups; -
    • -
    -

    Be careful: a change request is created when at least one predicate matches, so an - exempted user - still need a change request to modify a node from a supervised group. -

    -
    -

    Configure users with change validation

    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    -

    - Any change done by a validated user will be automatically deployed without validation needed - by another user. -

    -
    -
    -
    - -
    -
    -
    -
    -
    -
      -
    • -
      - - -
      -
    • -
    - - [messages] -
    -
    -
    -
    -
    - -
    -

    - Any change done by a Validated User will be automatically approved no matter the nature of the change. -

    -

    - Configuring groups below will hence have no effect on validated users (in the list above), but will apply to non-validated users, who will still need a change request to modify a node from a supervised group. -

    -
    -
    -
    - -
    -
    -
    -

    Configure groups with change validations

    -
    -
    -
    -
    -
    - -
    -
    -
    -
    - -
    -

    - Change validation are enable for any change that would impact a node belonging to one - of the chosen groups below. - Be careful: a change on one another group -

    -

    - The supervised changes are: -

      -
    • any change in a global parameter, as these changes can have side effects spreading - technique code,
    • -
    • any modification in one of the supervised groups,
    • -
    • any change in a rule which targets a node which belong to a group marked as supervised, -
    • -
    • any change in a directive used in one of the previous rules.
    • -
    -

    -

    - Changes in techniques are not subjected to change validation, nor are changes resulting from - an archive import. -

    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - + diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeValidationSettings.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeValidationSettings.scala index 4b249677f..75fb5d44b 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeValidationSettings.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeValidationSettings.scala @@ -45,7 +45,6 @@ import net.liftweb.common.* import net.liftweb.http.* import net.liftweb.http.js.JsCmds.Run import net.liftweb.http.js.JsCmds.Script -import net.liftweb.util.Helpers import net.liftweb.util.Helpers.* import scala.xml.NodeSeq @@ -163,13 +162,8 @@ class ChangeValidationSettings extends DispatchSnippet { initSelfVal match { case Full(_) => - val tooltipid = Helpers.nextFuncName - - - -
    - Allow users to validate Change Requests they created themselves? Validating is moving a Change Request to the "Pending deployment" status -
    + val tooltipMsg = """Allow users to validate Change Requests they created themselves? Validating is moving a Change Request to the "Pending deployment" status""" + case _ => NodeSeq.Empty } } & @@ -177,13 +171,8 @@ class ChangeValidationSettings extends DispatchSnippet { "#selfDepTooltip *" #> { initSelfDep match { case Full(_) => - val tooltipid = Helpers.nextFuncName - - - -
    - Allow users to deploy Change Requests they created themselves? Deploying is effectively applying a Change Request in the "Pending deployment" status. -
    + val tooltipMsg = """Allow users to deploy Change Requests they created themselves? Deploying is effectively applying a Change Request in the "Pending deployment" status.""" + case _ => NodeSeq.Empty } } & From f63a1cc0527d6f0bfe7a3db54334b3b5bc9d78af Mon Sep 17 00:00:00 2001 From: "Francois @fanf42 Armand" Date: Fri, 21 Mar 2025 22:46:29 +0100 Subject: [PATCH 33/65] Fixes #26594: Impact of #26538 on api-authorization --- .../plugins/apiauthorizations/UserTokenApiDefinition.scala | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/api-authorizations/src/main/scala/com/normation/plugins/apiauthorizations/UserTokenApiDefinition.scala b/api-authorizations/src/main/scala/com/normation/plugins/apiauthorizations/UserTokenApiDefinition.scala index 51ea514d5..874766d69 100644 --- a/api-authorizations/src/main/scala/com/normation/plugins/apiauthorizations/UserTokenApiDefinition.scala +++ b/api-authorizations/src/main/scala/com/normation/plugins/apiauthorizations/UserTokenApiDefinition.scala @@ -42,6 +42,7 @@ import com.normation.errors.* import com.normation.eventlog.ModificationId import com.normation.rudder.api.* import com.normation.rudder.facts.nodes.NodeSecurityContext +import com.normation.rudder.repository.ldap.JsonApiAuthz import com.normation.rudder.rest.* import com.normation.rudder.rest.UserApi import com.normation.rudder.rest.data.* @@ -198,7 +199,7 @@ object UserApiImpl { expirationDate: Option[String], expirationDateDefined: Boolean, authorizationType: Option[ApiAuthorizationKind], - acl: Option[List[JsonApiPerm]] + acl: Option[List[JsonApiAuthz]] ) object RestApiAccount extends ApiAccountCodecs { @@ -220,13 +221,13 @@ object UserApiImpl { } } - def acl: Option[List[JsonApiPerm]] = { + def acl: Option[List[JsonApiAuthz]] = { import ApiAuthorization.* account.kind match { case PublicApi(authz, expirationDate) => authz match { case None | RO | RW => Option.empty - case ACL(acls) => Some(acls.flatMap(x => x.actions.map(a => JsonApiPerm(x.path.value, a.name)))) + case ACL(acls) => Some(acls.map(x => JsonApiAuthz(x.path.value, x.actions.toList.map(_.name)))) } case User | System => Option.empty } From 040066ad0031f06d724c9b6373639c5d84610e8c Mon Sep 17 00:00:00 2001 From: "Francois @fanf42 Armand" Date: Mon, 24 Mar 2025 19:05:42 +0100 Subject: [PATCH 34/65] Fixes #26608: CurrentUser.queryContext is null when used in a ZIO for (public plugins) --- .../snippet/ChangeRequestDetails.scala | 8 +++++--- .../extension/CreateNodeDetailsExtension.scala | 7 +++++-- .../OpenScapNodeDetailsExtension.scala | 17 +++++++++++------ 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestDetails.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestDetails.scala index 965893219..39eb72513 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestDetails.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestDetails.scala @@ -114,13 +114,15 @@ class ChangeRequestDetails extends DispatchSnippet with Loggable { } private[this] def step = changeRequest.flatMap(cr => workflowService.findStep(cr.id)) +implicit private val qc: QueryContext = CurrentUser.queryContext // bug https://issues.rudder.io/issues/26605 + def dispatch = { // Display Change request Header case "header" => (xml => { changeRequest match { case eb: EmptyBox => NodeSeq.Empty - case Full(cr) => displayHeader(cr)(CurrentUser.queryContext) + case Full(cr) => displayHeader(cr) } }) @@ -145,7 +147,7 @@ class ChangeRequestDetails extends DispatchSnippet with Loggable { cr.owner, step, cr.id, - changeDetailsCallback(cr)(_)(CurrentUser.queryContext) + changeDetailsCallback(cr)(_) ).display } }) @@ -165,7 +167,7 @@ class ChangeRequestDetails extends DispatchSnippet with Loggable { _ => { changeRequest match { case eb: EmptyBox => NodeSeq.Empty - case Full(cr) => displayWarnUnmergeable(cr)(CurrentUser.queryContext) + case Full(cr) => displayWarnUnmergeable(cr) } } ) diff --git a/node-external-reports/src/main/scala/com/normation/plugins/nodeexternalreports/extension/CreateNodeDetailsExtension.scala b/node-external-reports/src/main/scala/com/normation/plugins/nodeexternalreports/extension/CreateNodeDetailsExtension.scala index e2956ba50..bfd5c3d05 100644 --- a/node-external-reports/src/main/scala/com/normation/plugins/nodeexternalreports/extension/CreateNodeDetailsExtension.scala +++ b/node-external-reports/src/main/scala/com/normation/plugins/nodeexternalreports/extension/CreateNodeDetailsExtension.scala @@ -40,6 +40,7 @@ import com.normation.plugins.PluginExtensionPoint import com.normation.plugins.PluginStatus import com.normation.plugins.nodeexternalreports.service.NodeExternalReport import com.normation.plugins.nodeexternalreports.service.ReadExternalReports +import com.normation.rudder.facts.nodes.QueryContext import com.normation.rudder.users.CurrentUser import com.normation.rudder.web.components.ShowNodeDetailsFromNode import net.liftweb.common.* @@ -52,6 +53,8 @@ class CreateNodeDetailsExtension(externalReport: ReadExternalReports, val status val ttag: ClassTag[ShowNodeDetailsFromNode] ) extends PluginExtensionPoint[ShowNodeDetailsFromNode] with Loggable { + implicit private val qc: QueryContext = CurrentUser.queryContext // bug https://issues.rudder.io/issues/26605 + def pluginCompose(snippet: ShowNodeDetailsFromNode): Map[String, NodeSeq => NodeSeq] = Map( "popupDetails" -> addExternalReportTab(snippet) _, "mainDetails" -> addExternalReportTab(snippet) _ @@ -62,9 +65,9 @@ class CreateNodeDetailsExtension(externalReport: ReadExternalReports, val status * - add an li in ul with id=ruleDetailsTabMenu * - add the actual tab after the div with id=ruleDetailsEditTab */ - def addExternalReportTab(snippet: ShowNodeDetailsFromNode)(xml: NodeSeq) = { + def addExternalReportTab(snippet: ShowNodeDetailsFromNode)(xml: NodeSeq)(implicit qc: QueryContext) = { - val (tabTitle, content) = externalReport.getExternalReports(snippet.nodeId)(CurrentUser.queryContext) match { + val (tabTitle, content) = externalReport.getExternalReports(snippet.nodeId) match { case eb: EmptyBox => val e = eb ?~! "Can not display external reports for that node" ("External reports",
    {e.messageChain}
    ) diff --git a/openscap/src/main/scala/com/normation/plugins/openscappolicies/extension/OpenScapNodeDetailsExtension.scala b/openscap/src/main/scala/com/normation/plugins/openscappolicies/extension/OpenScapNodeDetailsExtension.scala index 5f57b41a9..54a6e2447 100644 --- a/openscap/src/main/scala/com/normation/plugins/openscappolicies/extension/OpenScapNodeDetailsExtension.scala +++ b/openscap/src/main/scala/com/normation/plugins/openscappolicies/extension/OpenScapNodeDetailsExtension.scala @@ -42,6 +42,7 @@ import com.normation.plugins.PluginStatus import com.normation.plugins.openscappolicies.OpenScapReport import com.normation.plugins.openscappolicies.services.OpenScapReportReader import com.normation.plugins.openscappolicies.services.ReportSanitizer +import com.normation.rudder.facts.nodes.QueryContext import com.normation.rudder.users.CurrentUser import com.normation.rudder.web.components.ShowNodeDetailsFromNode import com.normation.zio.* @@ -63,22 +64,26 @@ class OpenScapNodeDetailsExtension( )(implicit val ttag: ClassTag[ShowNodeDetailsFromNode]) extends PluginExtensionPoint[ShowNodeDetailsFromNode] with Loggable { - def pluginCompose(snippet: ShowNodeDetailsFromNode): Map[String, NodeSeq => NodeSeq] = Map( - "popupDetails" -> addOpenScapReportTab(snippet) _, - "mainDetails" -> addOpenScapReportTab(snippet) _ - ) + def pluginCompose(snippet: ShowNodeDetailsFromNode): Map[String, NodeSeq => NodeSeq] = { + implicit val qc: QueryContext = CurrentUser.queryContext // bug https://issues.rudder.io/issues/26605 + + Map( + "popupDetails" -> addOpenScapReportTab(snippet) _, + "mainDetails" -> addOpenScapReportTab(snippet) _ + ) + } /** * Add a tab: * - add an li in ul with id=openScapExtensionTab */ - def addOpenScapReportTab(snippet: ShowNodeDetailsFromNode)(xml: NodeSeq): NodeSeq = { + def addOpenScapReportTab(snippet: ShowNodeDetailsFromNode)(xml: NodeSeq)(implicit qc: QueryContext): NodeSeq = { // Actually extend def display(): NodeSeq = { val nodeId = snippet.nodeId val content = { (for { - info <- openScapReader.getOpenScapReportFile(nodeId)(CurrentUser.queryContext) + info <- openScapReader.getOpenScapReportFile(nodeId) report <- ZIO.foreach(info) { case (hostname, file) => openScapReader.getOpenScapReportContent(nodeId, hostname, file) } } yield { report From 14eaf26e5b5efd9350451a37d100e70ed987bd6a Mon Sep 17 00:00:00 2001 From: Raphael Gauthier Date: Wed, 19 Mar 2025 14:45:39 +0100 Subject: [PATCH 35/65] Fixes #26511: Branding plugin display is broken --- branding/src/main/elm/sources/View.elm | 35 +-- branding/src/main/style/media.css | 395 ------------------------- branding/src/main/style/media.scss | 389 ++++++++++++++++++++++++ 3 files changed, 405 insertions(+), 414 deletions(-) delete mode 100755 branding/src/main/style/media.css create mode 100755 branding/src/main/style/media.scss diff --git a/branding/src/main/elm/sources/View.elm b/branding/src/main/elm/sources/View.elm index 6bd8e573c..da5348618 100644 --- a/branding/src/main/elm/sources/View.elm +++ b/branding/src/main/elm/sources/View.elm @@ -2,9 +2,9 @@ module View exposing (barStyle, checkbox, createPanel, customBar, customBarPrevi import Color exposing (Color) import DataTypes exposing (..) -import Html exposing (..) -import Html.Attributes exposing (..) -import Html.Events exposing (..) +import Html exposing (Html, Attribute, a, div, label, input, span, i, form, button, h4, text) +import Html.Attributes exposing (id, class, style, type_, checked, for, href, attribute, value) +import Html.Events exposing (onClick, onInput) import ColorPicker @@ -29,7 +29,7 @@ view model = Nothing -> ( "(No file chosen)" , (UploadFile "small"), "upload-btn" ) customLogos = - [ div [ class "panel-col col-md-6" ] + [ div [ class "card-col col-md-6" ] [ div [ class "form-group" ] [ div [ class "input-group" ] [ label [ for "enable-wide-logo", class "input-group-text" ] @@ -47,7 +47,7 @@ view model = ] ] ] - , div [ class "panel-col col-md-6 bg-pattern" ][ viewPreview settings.smallLogo settings.wideLogo] + , div [ class "card-col col-md-6 bg-pattern " ][ viewPreview settings.smallLogo settings.wideLogo] ] viewPreview : Logo -> Logo -> Html msg @@ -69,7 +69,7 @@ view model = ] bar = - [ div [ class "panel-col col-md-6" ] + [ div [ class "card-col col-md-6" ] [ checkbox settings.displayBar ToggleCustomBar "enable-bar" "Display custom bar" , div [ class "form-group" ] [ label [] [ text "Background color" ] @@ -89,18 +89,18 @@ view model = ] , textField ToggleLabel EditLabelText settings.displayLabel settings.labelTxt "text-label" "Label text" ] - , div [ class "panel-col col-md-6 bg-pattern" ] [ customBarPreview model.settings ] + , div [ class "card-col col-md-6 bg-pattern" ] [ customBarPreview model.settings ] ] loginPage = - [ div [ class "panel-col col-md-6" ] + [ div [ class "card-col col-md-6" ] [ checkbox settings.displayBarLogin ToggleCustomBarLogin "display-bar" "Display custom bar" , textField ToggleMotd EditMotd settings.displayMotd settings.motd "text-motd" "MOTD" ] - , div [ class "panel-col col-md-6 bg-pattern" ] [ loginPagePreview model.settings ] + , div [ class "card-col col-md-6 bg-pattern" ] [ loginPagePreview model.settings ] ] in - Html.form [] + div [class "d-flex flex-column p-3 pb-5"] [ createPanel "Custom logos" "custom-logos" customLogos , createPanel "Custom bar" "custom-bar" bar , createPanel "Login page" "login-page" loginPage @@ -156,17 +156,14 @@ fileField msg inputId txt = createPanel : String -> String -> List (Html msg) -> Html msg createPanel panelTitle panelId bodyContent = - let - hrefId = "#" ++ panelId - in - div [ class "panel panel-default" ] - [ div [ class "panel-heading" ] - [ h4 [ class "panel-title" ] - [ a [ attribute "data-bs-toggle" "collapse", attribute "data-bs-target" hrefId, href hrefId ] [ text panelTitle ] + div [ class "card rounded-3 mb-3" ] + [ div [ class "card-header p-0" ] + [ h4 [class "m-0"] + [ a [ attribute "data-bs-toggle" "collapse", href ("#" ++ panelId) ] [ text panelTitle ] ] ] - , div [ id panelId, class "panel-collapse collapse show" ] - [ div [ class "panel-body" ] bodyContent ] + , div [ id panelId, class "collapse show" ] + [ div [ class "card-body p-0 d-flex" ] bodyContent ] ] diff --git a/branding/src/main/style/media.css b/branding/src/main/style/media.css deleted file mode 100755 index a730e01d7..000000000 --- a/branding/src/main/style/media.css +++ /dev/null @@ -1,395 +0,0 @@ -html, body { - background: #fff; - padding: 0; - margin: 0; -} -main{ - padding-top: 20px; -} -.bg-pattern{ - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAlcAAAGQCAYAAACDPkUVAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gINDzsW3MiGwAAAIABJREFUeNrt3VFS47oSBmBZxRbOxftfXTjeg3UfKPl0FCm2hzAE+HgZICr5i3mYrlb7z1RKSfXr7e2tpJTSNE3pf//735QGX3VdSim9vr7erCulpGmartb973//m6Zpunq9fv3777+lOnr78T2/79513T8+Pj4+vp/oG+2Xewt6sPrm/v3335uNYoFW31x7wYhpb9y6rodvHN9z+u4VVu4fHx8fH99P843+HyylvBdXseJrF7SVXKwg6/fTNF2tHb3RepPairS97r2KlO+5fEc6Vu4fHx8fH99P8/WuW383XS6Xw620UkrKOad//vnnqpIb3ZAWlnO+W/G17bazrb7f7vtoq/QjviOtV39fPj4+Pr6f4mvXRWM+U6HlnK/OHtu2W2zhtW80wnqtubaCvNfq4/v3VGH1N3y9/fx9+fj4+Ph+qm9UWKWU0su9Cq1esL72zz//3FSQ9fu2RVZ/f3Y4ra4dVYZ8Hxvue5Rv7yjQ35ePj4+P76f76pp2Fmtqh7n2jnZ6VVr7Rnvtu7NHWbWF96dT/3yf5/uT/dw/Pj4+Pr6f5hs9PZjjgvh9u1Fvon7UmuvBzh5ltTeO73l8Z44C3T8+Pj4+vp/qi8eVcb/cXmy0UXy9t9HojT7iKIvv632fnWPl78vHx8fH9x19vfiGm86VHA0+OVZ8fHx8fHznfKWUbV2OZ4ZyNPjkWPHx8fHx8e376n61IJvn+b+nG2OLTI6GHCs5Vnx8fHx8fPu+ul/tWMUnDHNspcnRkGMlx4qPj4+Pj2/ft65rmqYpzfO8XXcLEY2ttJgD0TvLjC2y2GbrVXKjwqD3+GNc1+ZU8J1rlT7Kt3cU6O/Lx8fHx/fbffW6bTG3ffyNHA0+OVZ8fHx8fHwf9+W6kRwNPjlWfHx8fHx8x3yjEPa3t7eS5VTw7V3X/ePj4+Pj47v2tXNYVwVY72JyNORYuX98fHx8fHz3fW1Ce/05ty/K0ZBj5f7x8fHx8fHt+2JBV0pJ67q+Pz04apHJ0fj7vrOt0kf67lXq/r58fHx8fHzH/5+++vgbORpf6zvaipRjxcfHx8fH9zy+dt1LrdbqgpgDEdNG28pwVBjsPd7f5lREWMypqPv8Nt9exfwo395RoL8vHx8fHx/fvq87hB/PKOVU/A7fn+zn/vHx8fHx8d02QHpHlVmOxu/znTkKdP/4+Pj4+Pj6vtH/q7m3QI6GHCv3j4+Pj4+P77xvi2KQoyHHyv3j4+Pj4+M75+t9LctSpvqpzindPq740YqvHSrrtfrawbR2iC0+HcB33veZHSt/Xz4+Pj6+3+5r9yulpO0sU06FHCt/Xz4+Pj4+vvO+OotVX9s0cirkWPn78vHx8fHxHfPF/ZZl2Qqr19fX6UVOhRwrf18+Pj4+Pr5zvvrv5XK5+f93GlVycjTkWPn78vHx8fHxjX2Xy+WqY1ULtClOwT9i+KtX8e0Nfx25IXx8fHx8fHx838GXHwGL7bHRmaccDTlW/r58fHx8fD/FFztWrS3LqZBj5f7x8fHx8fGd863rOnwaMddfHK344rp60ZgDESvI+n2c0r8XHd+b0ue77zvSsXL/+Pj4+Pj4Huub53nqFX0ppfTSVmhnciDixHybA9FerH6/LMvulP7oyOvZfX/ainyE78hZsL8vHx8fHx/f43yj62Y5Go/zHW1FyrHi4+Pj4+P73r62sIqdrdxu0nv8Mb6BNgcittJiDkTvLDO28GKbrdfqGxUuz+7ba0U+yrd3FPhd7x8fHx8fH9938PUKui1/q8XXBXI05Fj5+/Lx8fHx8fV99/KuboorORV8fHx8fHx8fG+nR4DqunwEFtttozNPORpyrPx9+fj4+Ph+k2/0/3TuLZCjIcfK35ePj4+Pj2/sG/0/XUp5L67kaDyvT44VHx8fHx/f8/l6162/m+KnOe8Nf8UciN75ZHtDPjun4m/7zrYiH+k7csb77PePj4+Pj4/vp/juzXblMxWaHKtjrUg5Vnx8fHx8fD/bNyqsUkrp5V6F1k7VxxyI9lHEtkVWf99e8E9yKp7Jt1e5Psq3dxT4Xe8fHx8fHx/fT/HVNe0sVjfn6sjRmBwNOVZ8fHx8fHy/2deuv0lob1NJ2416E/Wj1lwPdvaorb1xv8l35ijQ/ePj4+Pj4/saXzyujPvl9mKjjeRoyLHy9+Xj4+Pj47s9rmz/P7/pXMnR+BrfvcLK/ePj4+Pj43tuXynlOqFdjoYcK39fPj4+Pj6+4766Xy3I5nn+7+nG2CKTY3WuFflI371KXQ4JHx8fHx/fc/nqfrVjFZ8wzLGVJsfqWCtSjhUfHx8fH9/v9q3rmqZpSvM8b9fdQkRjKy3mQPTOMmOLLLbZepXcqHDpPf4Y17U5FX/bt9eKfJRv7yjwu94/Pj4+Pj6+3+Kr122Lue3jb+Ro/D3fn+zn/vHx8fHx8X0PX64bydH4e74zR4HuHx8fHx8f3/P5RiHsb29vJcup+Ds+OVZ8fHx8fHw/x9fOYV0VYL2LydGQY8XHx8fHx8d339cmtNefc/uiHA05Vnx8fHx8fHz7vljQlVLSuq7vTw+OWmRyrB7ru1cJyyHh4+Pj4+P7vr523dXH38ix+jxfb7/vdv/4+Pj4+Pj4+v+fx3UvtVqrC2IOREwbbSvDUeGyFz/Q5lREWMypqPs82rdXkT7Kt3cU+F3vHx8fHx8fH1+5uu7NLFY8o5RT8Rjfn+zn/vHx8fHx8X0vXw0Sba+Z5Wg83nfmKND94+Pj4+Pj+56+0f/7ubdAjoYcKz4+Pj4+Pr7zvi2KQY6GHCs+Pj4+Pj6+c77e17IsZaqf6pzS7eOKH6342qGyXquvHUxrh9ji0wHP6PvMjtVvuH98fHx8fHzf2dfuV0pJ21mmHCs5Vnx8fHx8fHznfXUWq762aeRYybHi4+Pj4+PjO+aL+y3LshVWr6+v04scKzlWfHx8fHx8fOd89d/L5XJTH0yjSk6OhhwrPj4+Pj4+vrHvcrlcdaxqgTbFKfhHDH/1Kr694a8jN4SPj4+Pj4+P7zv48iNgsT02OvOUYyWHhI+Pj4+P76f4YseqtWU5FXKs+Pj4+Pj4+M751nUdPo2Y6y+OVnxxXb1ozIGIFWT9Pk7p34uO703pf7XvSMfK/ePj4+Pj4/tdvnmep17Rl1JKL22FdiYHIk7MtzkQ7cXq98uy7E7pnzmS+1u+I2etz37/+Pj4+Pj4+B7nG103y7E65uvt1/rkkPDx8fHx8f0OX1tYxc5WbjfpPf4Y30CbAxFbaTEHoneWGVt4sc3Wa/Wd6Vh9hm/vKHDke/b7x8fHx8fHx/dxX6+g2/K3WnxdIMcqPa1PDgkfHx8fH9/X+u7lXd0UV3Iq+Pj4+Pj4+PjeTo8o1XX5CCy220ZnnnKs5JDw8fHx8fH9Jt+ojsi9BXKs0tP55JDw8fHx8fE9j29UR5RS3osrOVb3O1ZyPvj4+Pj4+PjaDljPN01TmuKnOe8Nf8UciN75ZHtDPjvH6pG+I2eoZ3M0/vb94+Pj4+Pj4/s7vnuzXflMhSbHSs4HHx8fHx8fXxoWViml9HKvQmun6mMORPsoYtsiq79vL/jR4bRH+faOAke+vfiGr75/fHx8fHx8fH/HV9e0s1jdnKsjR3dyrOR88PHx8fHx/WZfu/4mob1NJW036k3Uj1pzPdjZo8D2xj3Sd+Yo8Ct8z37/+Pj4+Pj4+K6PK+N+ub3YaCM5VnI++Pj4+Pj4+G6PK9t646ZzJcfqa3xySPj4+Pj4+L6vr5RyndAux0rOBx8fHx8fH99xX92vFmTzPP/3dGNskcmxkvPBx8fHx8fHt++r+9WOVXzCMMdWmhwrOR98fHx8fHx8+751XdM0TWme5+26W4hobKXFHIjeWWZskcU2W6+SO9OxiuvanIo/9e0dBY58vcczP8P3p/ePj4+Pj4+P7zl89bptMbd9/I0cq7/nk0PCx8fHx8f3c325biTH6u/55JDw8fHx8fF9b98ohP3t7a1kOVZ/xyeHhI+Pj4+P7+f42jmsqwKsdzE5VnI++Pj4+Pj4+O772oT2+nNuX5RjJeeDj4+Pj4+Pb98XC7pSSlrX9f3pwVGLTI7VY31ySPj4+Pj4+H6mr1139fE3cqw+zyeHhI+Pj4+P72f62nUvtVqrC2IOREwbbSvDP+1YtTkVERZzKuo1R769o8CRby++4VG+P71/fHx8fHx8fN/H1x3Cj2eUcqwe45PzwcfHx8fH9/N9NUi0vWaWY/V4nxwSPj4+Pj6+n+8b1SW5t0COlZwPPj4+Pj4+vvO+LYpBjpWcDz4+Pj4+Pr5zvt7Xsixlqp/qnNLt44ofrfjaobJeq68dTGuH2D6zY/UIX3x64RnvHx8fHx8fH9/n+dr9SilpO8uUYyXng4+Pj4+Pj++8r85i1dc2jRwrOR98fHx8fHx8x3xxv2VZtsLq9fV1epFjJeeDj4+Pj4+P75yv/nu5XG7ql2lUycmxkvPBx8fHx8fHN/ZdLperjlUt0KY4Bf+I4a9exbc3/HXkhvDx8fHx8fHxfQdffgQstsdGZ55yrPj4+Pj4+Ph+ii92rFpblmMl54OPj4+Pj4/vnG9d1+HTiLn+4mjFF9fVi8YciFhB1u/jlH4v2n6vY/WVvr2nCPj4+Pj4+Ph+n2+e56lX9KWU0ssjWmkRVnMg2ovV75dl+ePh9bM5FY/0fUaOBh8fHx8fH9/39Y2um+VYHfPJ+eDj4+Pj4+MbFVaxs5XPttLaHIjYSos5EL2zzCMtvD/JqfgMX2wD8vHx8fHx8fGNjgzrdbf8rRZfF8ixkvPBx8fHx8fH1/fdy7u6Ka7kVPDx8fHx8fHxvZ3+KMC6Lh+BxXbb6MxTjhUfHx8fHx/fb/KN6pzcWyDHSs4HHx8fHx8f39g3qnNKKe/FlRyr9y85H3x8fHx8fHxHfL3r1t9N8dOc94a/Yg5E73yyvSGfnWP1SN9n5Gjw8fHx8fHx/UzfvdmufKZCk2PFx8fHx8fHx5eGhVVKKb3cq9DaqfqYA9E+inivRdarDEdHhnvxDY/y1d+3N4SPj4+Pj4+P74ivrmlnsbo5V3vDWi1QjhUfHx8fHx/fb/O1628S2ttU0naj3kT9vdbc0aPA0Rtob9wjfb0bx8fHx8fHx8d3xhePK+N+ub3YaCM5Vnx8fHx8fHx8t8eVbT1007mSY8XHx8fHx8fHd85XSrlOaJdjxcfHx8fHx8d33Ff3qwXZPM//Pd0YW2RyrPj4+Pj4+Pj49n11v9qxik8Y5thKk2PFx8fHx8fHx7fvW9c1TdOU5nnerruFiMZWWsyB6J1lHmnh/UlORVzX5lT8qS+2Afn4+Pj4+Pj4Hu2r122Lue3jb+RY8fHx8fHx8fF93JfrRnKs+Pj4+Pj4+PiO+UYh7G9vbyXLseLj4+Pj4+PjO+dr57CuCrDexeRY8fHx8fHx8fHd97UJ7fXn3L4ox4qPj4+Pj4+Pb98XC7pSSlrX9f3pwVGLTI4VHx8fHx8fH9+xeiiuu/r4GzlWfHx8fHx8fHznfO26l1qt1QWfkWM1qiDrDamvxZyKus/IV3/fVpt8fHx8fHx8fH/L1x3Cj2eUcqz4+Pj4+Pj4+PLho8DeUWWWY8XHx8fHx8fHd943qpvykQvK0eDj4+Pj4+Pj2/dtUQxyrPj4+Pj4+Pj4zvl6X8uylGF8+yMqvnaorNfqawfT2iG2+HRA+zglHx8fHx8fH99X+tr9Sinp7mfj9CpIORp8fHx8fHx8fNfdrZrQnlKYuWrfQK/1JUeDj4+Pj4+Pj+/6yHJZlq2wen19nV7OHgXK0eDj4+Pj4+P77b767+VyuckJnUaVnBwNPj4+Pj4+Pr6x73K5XHWsaoE2xSn4Rwx/9Sq+veGvIzeEj4+Pj4+Pj+87+PIjYLE9NjrzlKPBx8fHx8fH91N8sWPV2rKcCj4+Pj4+Pj6+c751XYdPI+b6i6MVX1xXLxpzIGIFWb+PU/r3ouN7U/p8fHx8fHx8fM/mm+d56hV9KaX00lZoZ3Ig4sR8mwPRXqx+vyzL7pT+6CyTj4+Pj4+Pj+9ZfKPrZjkafHx8fHx8fHznfG1hFTtbNyGivccf4xtocyBiKy3mQPTOMmMLL7bZeq2+0RkqHx8fHx8fH99X+3oF3Za/1fv4GzkafHx8fHx8fHxj3728q5viSk4FHx8fHx8fH9/xgq5dl4/AYrttdOYpR4OPj4+Pj4/vN/lGBVjuLZCjwcfHx8fHx8c39o0Kq1LKe3ElR4OPj4+Pj4+P79xThj3fNE1pip/mvDf8FXMgeueT7Q357JwKPj4+Pj4+Pr6v8N2b7cpnKjQ5Gnx8fHx8fHx8aVhYpZRSlqPBx8fHx8fHx3feF48XY+HXzbm615rrVWlyNPj4+Pj4+Ph+m69df5PQ3lZz7Ua9ifpRa64HG7X6Rm+gvXF8fHx8fHx8fM/ki8eVcb/cXmy0kRwNPj4+Pj4+Pr7b48q2oMu9s8cWJkeDj4+Pj4+Pj2/sK6VcJ7TL0eDj4+Pj4+PjO+6r+9WCbJ7n/55ujC0yORp8fHx8fHx8fPu+ul/tWNVCbpqm986VHA0+Pj4+Pj4+vuO+dV3TNE1pnuftuluIqJwKPj4+Pj4+Pr7zvnrdtpjbPv5GjgYfHx8fHx8f38d9uW4kR4OPj4+Pj4+P75hvFML+9vZWspwKPj4+Pj4+Pr5zvnYO66oA611MjgYfHx8fHx8f331fm9Bef87ti3I0+Pj4+Pj4+Pj2fbGgK6WkdV3fnx4ctcjkaPDx8fHx8fHxTYeG6+O6q4+/kaPBx8fHx8fHx3fO1657qdVaXRBzIGLaaFsZjs4oR48/9irIekPqazGnou7Dx8fHx8fHx/esvu4QfjyjlFPBx8fHx8fHx5cPHwX2jiqzHA0+Pj4+Pj4+vvO+UQGWewvkaPDx8fHx8fHxnfdtUQxyNPj4+Pj4+Pj4zvl6X8uylKl+qnNKt48rfrTia4fKeq2+djCtHWKLTwfw8fHx8fHx8T2Tr92vlJK2s0w5FXx8fHx8fHx85311Fqu+tmnkVPDx8fHx8fHxHfPF/ZZl2Qqr19fX6UVOBR8fHx8fHx/fOV/993K53HxEzjSq5ORo8PHx8fHx8fGNfZfL5apjVQu0KU7BP2L4q1fx7Q1/HbkhfHx8fHx8fHzfwZcfAYvtsdGZpxwNPj4+Pj4+vp/iix2r1pblVPDx8fHx8fHxnfOt6zp8GjHXXxyt+OK6etGYAxEryPp9nNK/Fx3fm9Ln4+Pj4+Pj43s23zzPU6/oSymll7ZCO5MDESfm2xyI9mL1+2VZdqf0R2eZfHx8fHx8fHzP4htdN8vR4OPj4+Pj4+M752sLq9jZyu0mvccf4xtocyBiKy3mQPTOMmMLL7bZeq2+0RkqHx8fHx8fH99X+3oF3Za/1eLrAjkafHx8fHx8fHx93728q5viSk4FHx8fHx8fH9/xgq5dl4/AYrttdOYpR4OPj4+Pj4/vN/lGBVjuLZCjwcfHx8fHx8c39o0Kq1LKe3ElR4OPj4+Pj4+P79xThj3fNE1pip/mvDf8FXMgeueT7Q357JwKPj4+Pj4+Pr6v8N2b7cpnKjQ5Gnx8fHx8fHx8aVhYpZRSlqPBx8fHx8fHx3feF48XY+HXzbm615rrVWlyNPj4+Pj4+Ph+m69df5PQ3lZz7Ua9ifpRa64HG7X6Rm+gvXF8fHx8fHx8fM/ki8eVcb/cXmy0kRwNPj4+Pj4+Pr7b48q2oMu9s8cWJkeDj4+Pj4+Pj2/sK6VcJ7TL0eDj4+Pj4+PjO+6r+9WCbJ7n/55ujC0yORp8fHx8fHx8fPu+ul/tWNVCbpqm986VHA0+Pj4+Pj4+vuO+dV3TNE1pnuftuluIqJwKPj4+Pj4+Pr7zvnrdtpjbPv5GjgYfHx8fHx8f38d9uW4kR4OPj4+Pj4+P75hvFML+9vZWspwKPj4+Pj4+Pr5zvnYO66oA611MjgYfHx8fHx8f331fm9Bef87ti3I0+Pj4+Pj4+Pj2fbGgK6WkdV3fnx4ctcjkaPDx8fHx8fHxTYeG6+O6q4+/kaPBx8fHx8fHx3fO1657qdVaXRBzIGLaaFsZjs4oR48/9irIekPqazGnou7Dx8fHx8fHx/esvu4QfjyjlFPBx8fHx8fHx5cPHwX2jiqzHA0+Pj4+Pj4+vvO+UQGWewvkaPDx8fHx8fHxnfdtUQxyNPj4+Pj4+Pj4zvl6X8uylKl+qnNKt48rfrTia4fKeq2+djCtHWKLTwfw8fHx8fHx8T2Tr92vlJK2s0w5FXx8fHx8fHx85311Fqu+tmnkVPDx8fHx8fHxHfPF/ZZl2Qqr19fX6UVOBR8fHx8fHx/fOV/993K53HxEzjSq5ORo8PHx8fHx8fGNfZfL5apjVQu0KU7BP2L4q1fx7Q1/HbkhfHx8fHx8fHzfwZcfAYvtsdGZpxwNPj4+Pj4+vp/iix2r1pblVPDx8fHx8fHxnfOt6zp8GjHXXxyt+OK6etGYAxEryPp9nNK/Fx3fm9Ln4+Pj4+Pj43s23zzPU6/oSymll7ZCO5MDESfm2xyI9mL1+2VZdqf0R2eZfHx8fHx8fHzP4htdN8vR4OPj4+Pj4+M752sLq9jZyu0mvccf4xtocyBiKy3mQPTOMmMLL7bZeq2+0RkqHx8fHx8fH99X+3oF3Za/1eLrAjkafHx8fHx8fHx93728q5viSk4FHx8fHx8fH9/xgq5dl4/AYrttdOYpR4OPj4+Pj4/vN/lGBVjuLZCjwcfHx8fHx8c39o0Kq1LKe3ElR4OPj4+Pj4+P79xThj3fNE1pip/mvDf8FXMgeueT7Q357JwKPj4+Pj4+Pr6v8N2b7cpnKjQ5Gnx8fHx8fHx8aVhYpZRSlqPBx8fHx8fHx3feF48XY+HXzbm615rrVWlyNPj4+Pj4+Ph+m69df5PQ3lZz7Ua9ifpRa64HG7X6Rm+gvXF8fHx8fHx8fM/ki8eVcb/cXmy0kRwNPj4+Pj4+Pr7b48q2oMu9s8cWJkeDj4+Pj4+Pj2/sK6VcJ7TL0eDj4+Pj4+PjO+6r+9WCbJ7n/55ujC0yORp8fHx8fHx8fPu+ul/tWNVCbpqm986VHA0+Pj4+Pj4+vuO+dV3TNE1pnuftuluIqJwKPj4+Pj4+Pr7zvnrdtpjbPv5GjgYfHx8fHx8f38d9uW4kR4OPj4+Pj4+P75hvFML+9vZWspwKPj4+Pj4+Pr5zvnYO66oA611MjgYfHx8fHx8f331fm9Bef87ti3I0+Pj4+Pj4+Pj2fbGgK6WkdV3fnx4ctcjkaPDx8fHx8fHxTYeG6+O6q4+/kaPBx8fHx8fHx3fO1657qdVaXRBzIGLaaFsZjs4oR48/9irIekPqazGnou7Dx8fHx8fHx/esvu4QfjyjlFPBx8fHx8fHx5cPHwX2jiqzHA0+Pj4+Pj4+vvO+UQGWewvkaPDx8fHx8fHxnfdtUQxyNPj4+Pj4+Pj4zvl6X8uylKl+qnNKt48rfrTia4fKeq2+djCtHWKLTwfw8fHx8fHx8T2Tr92vlJK2s0w5FXx8fHx8fHx85311Fqu+tmnkVPDx8fHx8fHxHfPF/ZZl2Qqr19fX6UVOBR8fHx8fHx/fOV/993K53HxEzjSq5ORo8PHx8fHx8fGNfZfL5apjVQu0KU7BP2L4q1fx7Q1/HbkhfHx8fHx8fHzfwZcfAYvtsdGZpxwNPj4+Pj4+vp/iix2r1pblVPDx8fHx8fHxnfOt6zp8GjHXXxyt+OK6etGYAxEryPp9nNK/Fx3fm9Ln4+Pj4+Pj43s23zzPU6/oSymll7ZCO5MDESfm2xyI9mL1+2VZdqf0R2eZfHx8fHx8fHzP4htdN8vR4OPj4+Pj4+M752sLq9jZyu0mvccf4xtocyBiKy3mQPTOMmMLL7bZeq2+0RkqHx8fHx8fH99X+3oF3Za/1eLrAjkafHx8fHx8fHx93728q5viSk4FHx8fHx8fH9/xgq5dl4/AYrttdOYpR4OPj4+Pj4/vN/lGBVjuLZCjwcfHx8fHx8c39o0Kq1LKe3ElR4OPj4+Pj4+P79xThj3fNE1pip/mvDf8FXMgeueT7Q357JwKPj4+Pj4+Pr6v8N2b7cpnKjQ5Gnx8fHx8fHx8aVhYpZRSlqPBx8fHx8fHx3feF48XY+HXzbm615rrVWlyNPj4+Pj4+Ph+m69df5PQ3lZz7Ua9ifpRa64HG7X6Rm+gvXF8fHx8fHx8fM/ki8eVcb/cXmy0kRwNPj4+Pj4+Pr7b48q2oMu9s8cWJkeDj4+Pj4+Pj2/sK6VcJ7TL0eDj4+Pj4+PjO+6r+9WCbJ7n/55ujC0yORp8fHx8fHx8fPu+ul/tWNVCbpqm986VHA0+Pj4+Pj4+vuO+dV3TNE1pnuftuluIqJwKPj4+Pj4+Pr7zvnrdtpjbPv5GjgYfHx8fHx8f38d9uW4kR4OPj4+Pj4+P75hvFML+9vZWspwKPj4+Pj4+Pr5zvnYO66oA611MjgYfHx8fHx8f331fm9Bef87ti3I0+Pj4+Pj4+Pj2fbGgK6WkdV3fnx4ctcjkaPDx8fHx8fHxTYeG6+O6q4+/kaPBx8fHx8fHx3fO1657qdVaXRBzIGLaaFsZjs4oR48/9irIekPqazGnou7Dx8fHx8fHx/esvu4QfjyjlFPBx8fHx8fHx5cPHwX2jiqzHA0+Pj4+Pj4+vvO+UQGWewvkaPDx8fHx8fHxnfdtUQxyNPj4+Pj4+Pj4zvl6X8uylKl+qnNKt48rfrTia4fKeq2+djCtHWKLTwfw8fHx8fHx8T2Tr92vlJK2s0w5FXx8fHx8fHx85311Fqu+tmnkVPDx8fHx8fHxHfPF/ZZl2Qqr19fX6UVOBR8fHx8fHx/fOV/993K53HxEzjSq5ORo8PHx8fHx8fGNfZfL5apjVQu0KU7BP2L4q1fx7Q1/HbkhfHx8fHx8fHzfwZcfAYvtsdGZpxwNPj4+Pj6ZIRpyAAAINUlEQVQ+vp/iix2r1pblVPDx8fHx8fHxnfOt6zp8GjHXXxyt+OK6etGYAxEryPp9nNK/Fx3fm9Ln4+Pj4+Pj43s23zzPU6/oSymll7ZCO5MDESfm2xyI9mL1+2VZdqf0R2eZfHx8fHx8fHzP4htdN8vR4OPj4+Pj4+M752sLq9jZyu0mvccf4xtocyBiKy3mQPTOMmMLL7bZeq2+0RkqHx8fHx8fH99X+3oF3Za/1eLrAjkafHx8fHx8fHx93728q5viSk4FHx8fHx8fH9/xgq5dl4/AYrttdOYpR4OPj4+Pj4/vN/lGBVjuLZCjwcfHx8fHx8c39o0Kq1LKe3ElR4OPj4+Pj4+P79xThj3fNE1pip/mvDf8FXMgeueT7Q357JwKPj4+Pj4+Pr6v8N2b7cpnKjQ5Gnx8fHx8fHx8aVhYpZRSlqPBx8fHx8fHx3feF48XY+HXzbm615rrVWlyNPj4+Pj4+Ph+m69df5PQ3lZz7Ua9ifpRa64HG7X6Rm+gvXF8fHx8fHx8fM/ki8eVcb/cXmy0kRwNPj4+Pj4+Pr7b48q2oMu9s8cWJkeDj4+Pj4+Pj2/sK6VcJ7TL0eDj4+Pj4+PjO+6r+9WCbJ7n/55ujC0yORp8fHx8fHx8fPu+ul/tWNVCbpqm986VHA0+Pj4+Pj4+vuO+dV3TNE1pnuftuluIqJwKPj4+Pj4+Pr7zvnrdtpjbPv5GjgYfHx8fHx8f38d9uW4kR4OPj4+Pj4+P75hvFML+9vZWspwKPj4+Pj4+Pr5zvnYO66oA611MjgYfHx8fHx8f331fm9Bef87ti3I0+Pj4+Pj4+Pj2fbGgK6WkdV3fnx4ctcjkaPDx8fHx8fHxTYeG6+O6q4+/kaPBx8fHx8fHx3fO1657qdVaXRBzIGLaaFsZjs4oR48/9irIekPqazGnou7Dx8fHx8fHx/esvu4QfjyjlFPBx8fHx8fHx5cPHwX2jiqzHA0+Pj4+Pj4+vvO+UQGWewvkaPDx8fHx8fHxnfdtUQxyNPj4+Pj4+Pj4zvl6X8uylKl+qnNKt48rfrTia4fKeq2+djCtHWKLTwfw8fHx8fHx8T2Tr92vlJK2s0w5FXx8fHx8fHx85311Fqu+tmnkVPDx8fHx8fHxHfPF/ZZl2Qqr19fX6UVOBR8fHx8fHx/fOV/993K53HxEzjSq5ORo8PHx8fHx8fGNfZfL5apjVQu0KU7BP2L4q1fx7Q1/HbkhfHx8fHx8fHzfwZcfAYvtsdGZpxwNPj4+Pj4+vp/iix2r1pblVPDx8fHx8fHxnfOt6zp8GjHXXxyt+OK6etGYAxEryPp9nNK/Fx3fm9Ln4+Pj4+Pj43s23zzPU6/oSymll7ZCO5MDESfm2xyI9mL1+2VZdqf0R2eZfHx8fHx8fHzP4htdN8vR4OPj4+Pj4+M752sLq9jZyu0mvccf4xtocyBiKy3mQPTOMmMLL7bZeq2+0RkqHx8fHx8fH99X+3oF3Za/1eLrAjkafHx8fHx8fHx93728q5viSk4FHx8fHx8fH9/xgq5dl4/AYrttdOYpR4OPj4+Pj4/vN/lGBVjuLZCjwcfHx8fHx8c39o0Kq1LKe3ElR4OPj4+Pj4+P79xThj3fNE1pip/mvDf8FXMgeueT7Q357JwKPj4+Pj4+Pr6v8N2b7cpnKjQ5Gnx8fHx8fHx8aVhYpZRSlqPBx8fHx8fHx3feF48XY+HXzbm615rrVWlyNPj4+Pj4+Ph+m69df5PQ3lZz7Ua9ifpRa64HG7X6Rm+gvXF8fHx8fHx8fM/ki8eVcb/cXmy0kRwNPj4+Pj4+Pr7b48q2oMu9s8cWJkeDj4+Pj4+Pj2/sK6VcJ7TL0eDj4+Pj4+PjO+6r+9WCbJ7n/55ujC0yORp8fHx8fHx8fPu+ul/tWNVCbpqm986VHA0+Pj4+Pj4+vuO+dV3TNE1pnuftuluIqJwKPj4+Pj4+Pr7zvnrdtpjbPv5GjgYfHx8fHx8f38d9uW4kR4OPj4+Pj4+P75hvFML+9vZWspwKPj4+Pj4+Pr5zvnYO66oA611MjgYfHx8fHx8f331fm9Bef87ti3I0+Pj4+Pj4+Pj2fbGgK6WkdV3fnx4ctcjkaPDx8fHx8fHxTYeG6+O6q4+/kaPBx8fHx8fHx3fO1657qdVaXRBzIGLaaFsZjs4oR48/9irIekPqazGnou7Dx8fHx8fHx/esvu4QfjyjlFPBx8fHx8fHx5cPHwX2jiqzHA0+Pj4+Pj4+vvO+UQGWewvkaPDx8fHx8fHxnfdtUQxyNPj4+Pj4+Pj4zvl6X8uylKl+qnNKt48rfrTia4fKeq2+djCtHWKLTwfw8fHx8fHx8T2Tr92vlJK2s0w5FXx8fHx8fHx85311Fqu+tmnkVPDx8fHx8fHxHfPF/ZZl2Qqr19fX6UVOBR8fHx8fHx/fOV/993K53HxEzjSq5ORo8PHx8fHx8fGNfZfL5apjVQu0KU7BP2L4q1fx7Q1/HbkhfHx8fHx8fHzfwZcfAYvtsdGZpxwNPj4+Pj4+vp/iix2r1pblVPDx8fHx8fHxnfOt6zp8GjHXXxyt+OK6etGYAxEryPp9nNK/Fx3fm9Ln4+Pj4+Pj43s23zzPU6/oSymll7ZCO5MDESfm2xyI9mL1+2VZdqf0R2eZfHx8fHx8fHzP4htdN8vR4OPj4+Pj4+M752sLq9jZyu0mvccf4xtocyBiKy3mQPTOMmMLL7bZeq2+0RkqHx8fHx8fH99X+3oF3Za/1eLrAjkafHx8fHx8fHx93728q5viSk4FHx8fHx8fH9/xgq5d939udIhFzw2M+gAAAABJRU5ErkJggg==); -} -label.form-control{ - background-color: #F8F9FC; - box-shadow: none; - border-left: none; - padding: 8px 12px !important; -} -label.form-control,label.input-group-text{ - cursor: pointer; -} -label.form-control > i { - font-weight: normal; - opacity: .8; - font-family: var(--font-mono); - font-size: .9em; - margin-left: 2px; - font-style: normal; -} -form{ - padding: 15px; -} -.panel-body, .panel-heading{ - padding: 0; -} -.panel-body > .row > div{ - padding: 15px 30px; -} -.panel-heading a{ - padding: 10px 15px; - display: block; - text-decoration: none !important; - transition-duration: .2s; - border-bottom: 2px solid #D6DEEF; - background-color: #fff; - font-weight: bold; - transition-duration: .2s; - color: #041922 !important; - cursor: default; -} -.panel:hover .panel-heading a{ - border-bottom-color: #13beb7; -} -.panel-heading a.collapsed{ - border-bottom: 2px solid #D6DEEF; -} -/* -- PANEL SEPARATION -- */ -.panel-body > .panel-col:first-child{ - padding: 15px; - margin-right: -1px; - border-right: 1px solid #D6DEEF; - position: relative; - background-color: #fff; -} -.panel-body > .panel-col:last-child:before{ - content: ""; - z-index: 100; - position: absolute; - left: -1px; - top: calc(50% - 20px); - width: 0; - height: 0; - border-top: 20px solid transparent; - border-bottom: 20px solid transparent; - border-left: 20px solid #fff; - display: block; -} -.panel-body > .panel-col:last-child:after{ - content: ""; - z-index: 95; - position: absolute; - left: 0px; - top: calc(50% - 20px); - width: 0; - height: 0; - border-top: 20px solid transparent; - border-bottom: 20px solid transparent; - border-left: 20px solid #D6DEEF; - display: block; -} -.panel-body > .panel-col:last-child{ - padding: 15px; - border-left: 1px solid #D6DEEF; - background-color: #F8F9FC; - display: flex; -} - -@media screen and (min-width: 992px){ - .panel-body > .panel-col:first-child{ - margin-right: -1px; - border-right: 1px solid #D6DEEF; - } - .panel-body > .panel-col:last-child:before{ - left: -1px; - top: calc(50% - 20px); - width: 0; - height: 0; - border-top: 20px solid transparent; - border-bottom: 20px solid transparent; - border-left: 20px solid #fff; - } - .panel-body > .panel-col:last-child:after{ - left: 0px; - top: calc(50% - 20px); - width: 0; - height: 0; - border-top: 20px solid transparent; - border-bottom: 20px solid transparent; - border-left: 20px solid #D6DEEF; - } -} -/* -- --------------- -- */ -.preview-window{ - background-color: #fff; - margin:auto; - border:1px solid #D6DEEF; - width:100%; - max-width: 400px; - position: relative; -} -.preview-window .custom-bar{ - width: 100%; - height:20px; - text-align: center; - position: absolute; - top: 0; - left: 0; -} -.preview-window .custom-bar > span{ - font-size: 12px; - line-height: 20px; - font-weight: bold; -} -.preview-window > .left-menu{ - width: 20%; - height:220px; - background-color: #041922; - margin-top: -20px; -} -.preview-window > .top-menu{ - height: 25px; - background-color: #fff; - position: relative; - width: 100%; - margin-top: 20px; -} -.preview-window > .top-menu:after { - content: ""; - box-shadow: 2px 2px 5px #dce2e4b8; - position: absolute; - display: block; - top: 0; - left: 20%; - right: 0; - bottom: 0; -} -.preview-window > .custom-bar.hidden + .top-menu, .preview-window > .custom-bar.hidden + .top-menu + .left-menu{ - margin-top: 0px; -} -.preview-window > .top-menu > div{ - position: absolute; - left: 0; - top: 0; - height: 25px; - width: 20%; - background-color: #041922; -} -.preview-window > .top-menu > div > span{ - position: absolute; - left : 10px; - right : 10px; - top : 5px; - bottom: 5px; - background-size: contain; - background-position: center; - background-repeat: no-repeat; -} - -/* === LOGIN === */ -.preview-window.login{ - padding: 15px; - background-color: #F8F9FC; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} -.login .login-container{ - width: 62%; -} -.login .logo-container > div{ - height: 40px; - width: 100%; - background-size: contain; - background-position: center; - background-repeat: no-repeat; - margin: 12px 0 10px 0; -} -.preview-window .fake-form{ - width: 100%; - border-radius: 14px; - padding: 20px; - box-shadow: 0 10px 20px 10px #dce3ef9e; - position: relative; - margin-bottom: 25px; -} -.preview-window .fake-form .custom-bar{ - height:15px; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - font-weight: 700; - border-top-left-radius: 14px; - border-top-right-radius: 14px; -} -.preview-window .fake-form .custom-bar > span{ - line-height: 15px; -} -.preview-window .fake-form .text-motd{ - margin-top: 5px; - padding: 0 8px; - font-size: .9em; - text-align: center; -} -.preview-window .fake-form .fake-input{ - width: 90%; - margin: auto; - height: 20px; - border-radius: 4px; - border:1px solid #D6DEEF; - margin-top: 12px; -} -.preview-window .fake-form .fake-btn{ - background-color: #13beb7; - width: 90%; - margin: auto; - height: 20px; - border-radius: 4px; - margin-top: 12px; -} - -/* LOGOS PREVIEW */ -.preview-logo{ - width: 100%; - max-width: 370px; - display: flex; - margin: auto; - transition-property: opacity; - transition-duration: .2s; - height: 40px; - position: relative; - padding: 15px; - box-sizing: content-box; -} -.preview-logo:before, -.preview-logo:after{ - content:""; - position: absolute; - top: 0; - bottom: 0; - z-index: 1; - transition-property: opacity; - transition-duration: .2s; -} -.preview-logo:before{ - left: 0; - width: 70px; - background-color: #041922; -} -.preview-logo:after{ - left: 70px; - width: calc(100% - 70px); - background-color: #041922; -} -.preview-logo.sm-disabled:before, -.preview-logo.lg-disabled:after{ - opacity: .5; -} -.preview-logo > div{ - background-position: center; - background-size: contain; - background-repeat: no-repeat; - position: relative; - z-index: 2; -} -.preview-logo > div:first-child{ - width: 40px; -} -.preview-logo > div:last-child{ - flex: 1; -} - -/* -- COLOR PICKER -- */ -ui-dropdown-panel{ - z-index:9999 !important; -} -.input-group > .input-group-text + ui-picker{ - border-color: #D6DEEF; - border-top-left-radius: 0; - border-bottom-left-radius: 0; -} -.input-group > .input-group-text + ui-picker > ui-picker-input{ - padding: 0; - height: 100%; -} -.input-group > .input-group-text + ui-picker > ui-picker-input > ui-color-picker-rect{ - top: 0; - right: -1px; - width: 34px; - height: 33.5px; - border-top-left-radius: 0; - border-bottom-left-radius: 0; - padding: 11px 10px; - background-image: none; - background-color: #F8F9FC; - border: 1px solid #D6DEEF; - border-top: none; - border-bottom: none; -} -.input-group > .input-group-text + ui-picker ui-color-picker-background{ - border-radius: 2px; -} -.input-group > .input-group-text + ui-picker ui-color-picker-text{ - padding: 6px 12px; -} - -/* RUDDER CSS CONFLICTS FIXES */ -#branding-main{ - padding-bottom: 40px; -} -#branding-main .panel-heading, -#branding-main .panel-heading .panel-title{ - margin:0; - padding:0; -} -#branding-main .input-group > .input-group-text:last-child > .fa{ - color: #72829D; - transition-duration: .2s; -} -.remove-btn , -.upload-btn { - transition-duration: .2s; -} -#branding-main .input-group > .input-group-text:last-child.remove-btn:hover > i.fa{ - color: #DA291C; -} -#branding-main .input-group > .input-group-text:last-child.upload-btn:hover > i.fa{ - color: #337ab7; -} -#branding-main .input-group > .input-group-text:last-child.remove-btn > i.fa-upload, -#branding-main .input-group > .input-group-text:last-child.upload-btn > i.fa-times{ - display: none; -} - -#branding-main .toolbar { - background-color: #fff; - border-top: 1px solid #D6DEEF; - position: fixed; - z-index: 200; - text-align: right; - bottom: 0; - left: 0; - width: 100%; - padding: 10px 30px; -} -#branding-main .toolbar .btn{ - min-width: 80px; -} -#branding-main .panel{ - overflow: hidden; - border: 1px solid #D6DEEF; - margin-bottom: 20px; - border-radius: 4px; -} -#branding-main .panel-body{ - padding: 0; - display: flex; -} - -/* CSS EFFECTS */ -#headerBar > .background{ - transition-duration:.6s; - transition-property: color, background-color; -} \ No newline at end of file diff --git a/branding/src/main/style/media.scss b/branding/src/main/style/media.scss new file mode 100755 index 000000000..7d412dc81 --- /dev/null +++ b/branding/src/main/style/media.scss @@ -0,0 +1,389 @@ +// Colors +$rudder-bg-light-gray : #F8F9FC; +$rudder-border-color-default : #D6DEEF; + +#brandingTab{ + width: 100%; + background-color: $rudder-bg-light-gray; +} +.bg-pattern{ + position: relative; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAlcAAAGQCAYAAACDPkUVAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gINDzsW3MiGwAAAIABJREFUeNrt3VFS47oSBmBZxRbOxftfXTjeg3UfKPl0FCm2hzAE+HgZICr5i3mYrlb7z1RKSfXr7e2tpJTSNE3pf//735QGX3VdSim9vr7erCulpGmartb973//m6Zpunq9fv3777+lOnr78T2/79513T8+Pj4+vp/oG+2Xewt6sPrm/v3335uNYoFW31x7wYhpb9y6rodvHN9z+u4VVu4fHx8fH99P843+HyylvBdXseJrF7SVXKwg6/fTNF2tHb3RepPairS97r2KlO+5fEc6Vu4fHx8fH99P8/WuW383XS6Xw620UkrKOad//vnnqpIb3ZAWlnO+W/G17bazrb7f7vtoq/QjviOtV39fPj4+Pr6f4mvXRWM+U6HlnK/OHtu2W2zhtW80wnqtubaCvNfq4/v3VGH1N3y9/fx9+fj4+Ph+qm9UWKWU0su9Cq1esL72zz//3FSQ9fu2RVZ/f3Y4ra4dVYZ8Hxvue5Rv7yjQ35ePj4+P76f76pp2Fmtqh7n2jnZ6VVr7Rnvtu7NHWbWF96dT/3yf5/uT/dw/Pj4+Pr6f5hs9PZjjgvh9u1Fvon7UmuvBzh5ltTeO73l8Z44C3T8+Pj4+vp/qi8eVcb/cXmy0UXy9t9HojT7iKIvv632fnWPl78vHx8fH9x19vfiGm86VHA0+OVZ8fHx8fHznfKWUbV2OZ4ZyNPjkWPHx8fHx8e376n61IJvn+b+nG2OLTI6GHCs5Vnx8fHx8fPu+ul/tWMUnDHNspcnRkGMlx4qPj4+Pj2/ft65rmqYpzfO8XXcLEY2ttJgD0TvLjC2y2GbrVXKjwqD3+GNc1+ZU8J1rlT7Kt3cU6O/Lx8fHx/fbffW6bTG3ffyNHA0+OVZ8fHx8fHwf9+W6kRwNPjlWfHx8fHx8x3yjEPa3t7eS5VTw7V3X/ePj4+Pj47v2tXNYVwVY72JyNORYuX98fHx8fHz3fW1Ce/05ty/K0ZBj5f7x8fHx8fHt+2JBV0pJ67q+Pz04apHJ0fj7vrOt0kf67lXq/r58fHx8fHzH/5+++vgbORpf6zvaipRjxcfHx8fH9zy+dt1LrdbqgpgDEdNG28pwVBjsPd7f5lREWMypqPv8Nt9exfwo395RoL8vHx8fHx/fvq87hB/PKOVU/A7fn+zn/vHx8fHx8d02QHpHlVmOxu/znTkKdP/4+Pj4+Pj6vtH/q7m3QI6GHCv3j4+Pj4+P77xvi2KQoyHHyv3j4+Pj4+M75+t9LctSpvqpzindPq740YqvHSrrtfrawbR2iC0+HcB33veZHSt/Xz4+Pj6+3+5r9yulpO0sU06FHCt/Xz4+Pj4+vvO+OotVX9s0cirkWPn78vHx8fHxHfPF/ZZl2Qqr19fX6UVOhRwrf18+Pj4+Pr5zvvrv5XK5+f93GlVycjTkWPn78vHx8fHxjX2Xy+WqY1ULtClOwT9i+KtX8e0Nfx25IXx8fHx8fHx838GXHwGL7bHRmaccDTlW/r58fHx8fD/FFztWrS3LqZBj5f7x8fHx8fGd863rOnwaMddfHK344rp60ZgDESvI+n2c0r8XHd+b0ue77zvSsXL/+Pj4+Pj4Huub53nqFX0ppfTSVmhnciDixHybA9FerH6/LMvulP7oyOvZfX/ainyE78hZsL8vHx8fHx/f43yj62Y5Go/zHW1FyrHi4+Pj4+P73r62sIqdrdxu0nv8Mb6BNgcittJiDkTvLDO28GKbrdfqGxUuz+7ba0U+yrd3FPhd7x8fHx8fH9938PUKui1/q8XXBXI05Fj5+/Lx8fHx8fV99/KuboorORV8fHx8fHx8fG+nR4DqunwEFtttozNPORpyrPx9+fj4+Ph+k2/0/3TuLZCjIcfK35ePj4+Pj2/sG/0/XUp5L67kaDyvT44VHx8fHx/f8/l6162/m+KnOe8Nf8UciN75ZHtDPjun4m/7zrYiH+k7csb77PePj4+Pj4/vp/juzXblMxWaHKtjrUg5Vnx8fHx8fD/bNyqsUkrp5V6F1k7VxxyI9lHEtkVWf99e8E9yKp7Jt1e5Psq3dxT4Xe8fHx8fHx/fT/HVNe0sVjfn6sjRmBwNOVZ8fHx8fHy/2deuv0lob1NJ2416E/Wj1lwPdvaorb1xv8l35ijQ/ePj4+Pj4/saXzyujPvl9mKjjeRoyLHy9+Xj4+Pj47s9rmz/P7/pXMnR+BrfvcLK/ePj4+Pj43tuXynlOqFdjoYcK39fPj4+Pj6+4766Xy3I5nn+7+nG2CKTY3WuFflI371KXQ4JHx8fHx/fc/nqfrVjFZ8wzLGVJsfqWCtSjhUfHx8fH9/v9q3rmqZpSvM8b9fdQkRjKy3mQPTOMmOLLLbZepXcqHDpPf4Y17U5FX/bt9eKfJRv7yjwu94/Pj4+Pj6+3+Kr122Lue3jb+Ro/D3fn+zn/vHx8fHx8X0PX64bydH4e74zR4HuHx8fHx8f3/P5RiHsb29vJcup+Ds+OVZ8fHx8fHw/x9fOYV0VYL2LydGQY8XHx8fHx8d339cmtNefc/uiHA05Vnx8fHx8fHz7vljQlVLSuq7vTw+OWmRyrB7ru1cJyyHh4+Pj4+P7vr523dXH38ix+jxfb7/vdv/4+Pj4+Pj4+v+fx3UvtVqrC2IOREwbbSvDUeGyFz/Q5lREWMypqPs82rdXkT7Kt3cU+F3vHx8fHx8fH1+5uu7NLFY8o5RT8Rjfn+zn/vHx8fHx8X0vXw0Sba+Z5Wg83nfmKND94+Pj4+Pj+56+0f/7ubdAjoYcKz4+Pj4+Pr7zvi2KQY6GHCs+Pj4+Pj6+c77e17IsZaqf6pzS7eOKH6342qGyXquvHUxrh9ji0wHP6PvMjtVvuH98fHx8fHzf2dfuV0pJ21mmHCs5Vnx8fHx8fHznfXUWq762aeRYybHi4+Pj4+PjO+aL+y3LshVWr6+v04scKzlWfHx8fHx8fOd89d/L5XJTH0yjSk6OhhwrPj4+Pj4+vrHvcrlcdaxqgTbFKfhHDH/1Kr694a8jN4SPj4+Pj4+P7zv48iNgsT02OvOUYyWHhI+Pj4+P76f4YseqtWU5FXKs+Pj4+Pj4+M751nUdPo2Y6y+OVnxxXb1ozIGIFWT9Pk7p34uO703pf7XvSMfK/ePj4+Pj4/tdvnmep17Rl1JKL22FdiYHIk7MtzkQ7cXq98uy7E7pnzmS+1u+I2etz37/+Pj4+Pj4+B7nG103y7E65uvt1/rkkPDx8fHx8f0OX1tYxc5WbjfpPf4Y30CbAxFbaTEHoneWGVt4sc3Wa/Wd6Vh9hm/vKHDke/b7x8fHx8fHx/dxX6+g2/K3WnxdIMcqPa1PDgkfHx8fH9/X+u7lXd0UV3Iq+Pj4+Pj4+PjeTo8o1XX5CCy220ZnnnKs5JDw8fHx8fH9Jt+ojsi9BXKs0tP55JDw8fHx8fE9j29UR5RS3osrOVb3O1ZyPvj4+Pj4+PjaDljPN01TmuKnOe8Nf8UciN75ZHtDPjvH6pG+I2eoZ3M0/vb94+Pj4+Pj4/s7vnuzXflMhSbHSs4HHx8fHx8fXxoWViml9HKvQmun6mMORPsoYtsiq79vL/jR4bRH+faOAke+vfiGr75/fHx8fHx8fH/HV9e0s1jdnKsjR3dyrOR88PHx8fHx/WZfu/4mob1NJW036k3Uj1pzPdjZo8D2xj3Sd+Yo8Ct8z37/+Pj4+Pj4+K6PK+N+ub3YaCM5VnI++Pj4+Pj4+G6PK9t646ZzJcfqa3xySPj4+Pj4+L6vr5RyndAux0rOBx8fHx8fH99xX92vFmTzPP/3dGNskcmxkvPBx8fHx8fHt++r+9WOVXzCMMdWmhwrOR98fHx8fHx8+751XdM0TWme5+26W4hobKXFHIjeWWZskcU2W6+SO9OxiuvanIo/9e0dBY58vcczP8P3p/ePj4+Pj4+P7zl89bptMbd9/I0cq7/nk0PCx8fHx8f3c325biTH6u/55JDw8fHx8fF9b98ohP3t7a1kOVZ/xyeHhI+Pj4+P7+f42jmsqwKsdzE5VnI++Pj4+Pj4+O772oT2+nNuX5RjJeeDj4+Pj4+Pb98XC7pSSlrX9f3pwVGLTI7VY31ySPj4+Pj4+H6mr1139fE3cqw+zyeHhI+Pj4+P72f62nUvtVqrC2IOREwbbSvDP+1YtTkVERZzKuo1R769o8CRby++4VG+P71/fHx8fHx8fN/H1x3Cj2eUcqwe45PzwcfHx8fH9/N9NUi0vWaWY/V4nxwSPj4+Pj6+n+8b1SW5t0COlZwPPj4+Pj4+vvO+LYpBjpWcDz4+Pj4+Pr5zvt7Xsixlqp/qnNLt44ofrfjaobJeq68dTGuH2D6zY/UIX3x64RnvHx8fHx8fH9/n+dr9SilpO8uUYyXng4+Pj4+Pj++8r85i1dc2jRwrOR98fHx8fHx8x3xxv2VZtsLq9fV1epFjJeeDj4+Pj4+P75yv/nu5XG7ql2lUycmxkvPBx8fHx8fHN/ZdLperjlUt0KY4Bf+I4a9exbc3/HXkhvDx8fHx8fHxfQdffgQstsdGZ55yrPj4+Pj4+Ph+ii92rFpblmMl54OPj4+Pj4/vnG9d1+HTiLn+4mjFF9fVi8YciFhB1u/jlH4v2n6vY/WVvr2nCPj4+Pj4+Ph+n2+e56lX9KWU0ssjWmkRVnMg2ovV75dl+ePh9bM5FY/0fUaOBh8fHx8fH9/39Y2um+VYHfPJ+eDj4+Pj4+MbFVaxs5XPttLaHIjYSos5EL2zzCMtvD/JqfgMX2wD8vHx8fHx8fGNjgzrdbf8rRZfF8ixkvPBx8fHx8fH1/fdy7u6Ka7kVPDx8fHx8fHxvZ3+KMC6Lh+BxXbb6MxTjhUfHx8fHx/fb/KN6pzcWyDHSs4HHx8fHx8f39g3qnNKKe/FlRyr9y85H3x8fHx8fHxHfL3r1t9N8dOc94a/Yg5E73yyvSGfnWP1SN9n5Gjw8fHx8fHx/UzfvdmufKZCk2PFx8fHx8fHx5eGhVVKKb3cq9DaqfqYA9E+inivRdarDEdHhnvxDY/y1d+3N4SPj4+Pj4+P74ivrmlnsbo5V3vDWi1QjhUfHx8fHx/fb/O1628S2ttU0naj3kT9vdbc0aPA0Rtob9wjfb0bx8fHx8fHx8d3xhePK+N+ub3YaCM5Vnx8fHx8fHx8t8eVbT1007mSY8XHx8fHx8fHd85XSrlOaJdjxcfHx8fHx8d33Ff3qwXZPM//Pd0YW2RyrPj4+Pj4+Pj49n11v9qxik8Y5thKk2PFx8fHx8fHx7fvW9c1TdOU5nnerruFiMZWWsyB6J1lHmnh/UlORVzX5lT8qS+2Afn4+Pj4+Pj4Hu2r122Lue3jb+RY8fHx8fHx8fF93JfrRnKs+Pj4+Pj4+PiO+UYh7G9vbyXLseLj4+Pj4+PjO+dr57CuCrDexeRY8fHx8fHx8fHd97UJ7fXn3L4ox4qPj4+Pj4+Pb98XC7pSSlrX9f3pwVGLTI4VHx8fHx8fH9+xeiiuu/r4GzlWfHx8fHx8fHznfO26l1qt1QWfkWM1qiDrDamvxZyKus/IV3/fVpt8fHx8fHx8fH/L1x3Cj2eUcqz4+Pj4+Pj4+PLho8DeUWWWY8XHx8fHx8fHd943qpvykQvK0eDj4+Pj4+Pj2/dtUQxyrPj4+Pj4+Pj4zvl6X8uylGF8+yMqvnaorNfqawfT2iG2+HRA+zglHx8fHx8fH99X+tr9Sinp7mfj9CpIORp8fHx8fHx8fNfdrZrQnlKYuWrfQK/1JUeDj4+Pj4+Pj+/6yHJZlq2wen19nV7OHgXK0eDj4+Pj4+P77b767+VyuckJnUaVnBwNPj4+Pj4+Pr6x73K5XHWsaoE2xSn4Rwx/9Sq+veGvIzeEj4+Pj4+Pj+87+PIjYLE9NjrzlKPBx8fHx8fH91N8sWPV2rKcCj4+Pj4+Pj6+c751XYdPI+b6i6MVX1xXLxpzIGIFWb+PU/r3ouN7U/p8fHx8fHx8fM/mm+d56hV9KaX00lZoZ3Ig4sR8mwPRXqx+vyzL7pT+6CyTj4+Pj4+Pj+9ZfKPrZjkafHx8fHx8fHznfG1hFTtbNyGivccf4xtocyBiKy3mQPTOMmMLL7bZeq2+0RkqHx8fHx8fH99X+3oF3Za/1fv4GzkafHx8fHx8fHxj3728q5viSk4FHx8fHx8fH9/xgq5dl4/AYrttdOYpR4OPj4+Pj4/vN/lGBVjuLZCjwcfHx8fHx8c39o0Kq1LKe3ElR4OPj4+Pj4+P79xThj3fNE1pip/mvDf8FXMgeueT7Q357JwKPj4+Pj4+Pr6v8N2b7cpnKjQ5Gnx8fHx8fHx8aVhYpZRSlqPBx8fHx8fHx3feF48XY+HXzbm615rrVWlyNPj4+Pj4+Ph+m69df5PQ3lZz7Ua9ifpRa64HG7X6Rm+gvXF8fHx8fHx8fM/ki8eVcb/cXmy0kRwNPj4+Pj4+Pr7b48q2oMu9s8cWJkeDj4+Pj4+Pj2/sK6VcJ7TL0eDj4+Pj4+PjO+6r+9WCbJ7n/55ujC0yORp8fHx8fHx8fPu+ul/tWNVCbpqm986VHA0+Pj4+Pj4+vuO+dV3TNE1pnuftuluIqJwKPj4+Pj4+Pr7zvnrdtpjbPv5GjgYfHx8fHx8f38d9uW4kR4OPj4+Pj4+P75hvFML+9vZWspwKPj4+Pj4+Pr5zvnYO66oA611MjgYfHx8fHx8f331fm9Bef87ti3I0+Pj4+Pj4+Pj2fbGgK6WkdV3fnx4ctcjkaPDx8fHx8fHxTYeG6+O6q4+/kaPBx8fHx8fHx3fO1657qdVaXRBzIGLaaFsZjs4oR48/9irIekPqazGnou7Dx8fHx8fHx/esvu4QfjyjlFPBx8fHx8fHx5cPHwX2jiqzHA0+Pj4+Pj4+vvO+UQGWewvkaPDx8fHx8fHxnfdtUQxyNPj4+Pj4+Pj4zvl6X8uylKl+qnNKt48rfrTia4fKeq2+djCtHWKLTwfw8fHx8fHx8T2Tr92vlJK2s0w5FXx8fHx8fHx85311Fqu+tmnkVPDx8fHx8fHxHfPF/ZZl2Qqr19fX6UVOBR8fHx8fHx/fOV/993K53HxEzjSq5ORo8PHx8fHx8fGNfZfL5apjVQu0KU7BP2L4q1fx7Q1/HbkhfHx8fHx8fHzfwZcfAYvtsdGZpxwNPj4+Pj4+vp/iix2r1pblVPDx8fHx8fHxnfOt6zp8GjHXXxyt+OK6etGYAxEryPp9nNK/Fx3fm9Ln4+Pj4+Pj43s23zzPU6/oSymll7ZCO5MDESfm2xyI9mL1+2VZdqf0R2eZfHx8fHx8fHzP4htdN8vR4OPj4+Pj4+M752sLq9jZyu0mvccf4xtocyBiKy3mQPTOMmMLL7bZeq2+0RkqHx8fHx8fH99X+3oF3Za/1eLrAjkafHx8fHx8fHx93728q5viSk4FHx8fHx8fH9/xgq5dl4/AYrttdOYpR4OPj4+Pj4/vN/lGBVjuLZCjwcfHx8fHx8c39o0Kq1LKe3ElR4OPj4+Pj4+P79xThj3fNE1pip/mvDf8FXMgeueT7Q357JwKPj4+Pj4+Pr6v8N2b7cpnKjQ5Gnx8fHx8fHx8aVhYpZRSlqPBx8fHx8fHx3feF48XY+HXzbm615rrVWlyNPj4+Pj4+Ph+m69df5PQ3lZz7Ua9ifpRa64HG7X6Rm+gvXF8fHx8fHx8fM/ki8eVcb/cXmy0kRwNPj4+Pj4+Pr7b48q2oMu9s8cWJkeDj4+Pj4+Pj2/sK6VcJ7TL0eDj4+Pj4+PjO+6r+9WCbJ7n/55ujC0yORp8fHx8fHx8fPu+ul/tWNVCbpqm986VHA0+Pj4+Pj4+vuO+dV3TNE1pnuftuluIqJwKPj4+Pj4+Pr7zvnrdtpjbPv5GjgYfHx8fHx8f38d9uW4kR4OPj4+Pj4+P75hvFML+9vZWspwKPj4+Pj4+Pr5zvnYO66oA611MjgYfHx8fHx8f331fm9Bef87ti3I0+Pj4+Pj4+Pj2fbGgK6WkdV3fnx4ctcjkaPDx8fHx8fHxTYeG6+O6q4+/kaPBx8fHx8fHx3fO1657qdVaXRBzIGLaaFsZjs4oR48/9irIekPqazGnou7Dx8fHx8fHx/esvu4QfjyjlFPBx8fHx8fHx5cPHwX2jiqzHA0+Pj4+Pj4+vvO+UQGWewvkaPDx8fHx8fHxnfdtUQxyNPj4+Pj4+Pj4zvl6X8uylKl+qnNKt48rfrTia4fKeq2+djCtHWKLTwfw8fHx8fHx8T2Tr92vlJK2s0w5FXx8fHx8fHx85311Fqu+tmnkVPDx8fHx8fHxHfPF/ZZl2Qqr19fX6UVOBR8fHx8fHx/fOV/993K53HxEzjSq5ORo8PHx8fHx8fGNfZfL5apjVQu0KU7BP2L4q1fx7Q1/HbkhfHx8fHx8fHzfwZcfAYvtsdGZpxwNPj4+Pj4+vp/iix2r1pblVPDx8fHx8fHxnfOt6zp8GjHXXxyt+OK6etGYAxEryPp9nNK/Fx3fm9Ln4+Pj4+Pj43s23zzPU6/oSymll7ZCO5MDESfm2xyI9mL1+2VZdqf0R2eZfHx8fHx8fHzP4htdN8vR4OPj4+Pj4+M752sLq9jZyu0mvccf4xtocyBiKy3mQPTOMmMLL7bZeq2+0RkqHx8fHx8fH99X+3oF3Za/1eLrAjkafHx8fHx8fHx93728q5viSk4FHx8fHx8fH9/xgq5dl4/AYrttdOYpR4OPj4+Pj4/vN/lGBVjuLZCjwcfHx8fHx8c39o0Kq1LKe3ElR4OPj4+Pj4+P79xThj3fNE1pip/mvDf8FXMgeueT7Q357JwKPj4+Pj4+Pr6v8N2b7cpnKjQ5Gnx8fHx8fHx8aVhYpZRSlqPBx8fHx8fHx3feF48XY+HXzbm615rrVWlyNPj4+Pj4+Ph+m69df5PQ3lZz7Ua9ifpRa64HG7X6Rm+gvXF8fHx8fHx8fM/ki8eVcb/cXmy0kRwNPj4+Pj4+Pr7b48q2oMu9s8cWJkeDj4+Pj4+Pj2/sK6VcJ7TL0eDj4+Pj4+PjO+6r+9WCbJ7n/55ujC0yORp8fHx8fHx8fPu+ul/tWNVCbpqm986VHA0+Pj4+Pj4+vuO+dV3TNE1pnuftuluIqJwKPj4+Pj4+Pr7zvnrdtpjbPv5GjgYfHx8fHx8f38d9uW4kR4OPj4+Pj4+P75hvFML+9vZWspwKPj4+Pj4+Pr5zvnYO66oA611MjgYfHx8fHx8f331fm9Bef87ti3I0+Pj4+Pj4+Pj2fbGgK6WkdV3fnx4ctcjkaPDx8fHx8fHxTYeG6+O6q4+/kaPBx8fHx8fHx3fO1657qdVaXRBzIGLaaFsZjs4oR48/9irIekPqazGnou7Dx8fHx8fHx/esvu4QfjyjlFPBx8fHx8fHx5cPHwX2jiqzHA0+Pj4+Pj4+vvO+UQGWewvkaPDx8fHx8fHxnfdtUQxyNPj4+Pj4+Pj4zvl6X8uylKl+qnNKt48rfrTia4fKeq2+djCtHWKLTwfw8fHx8fHx8T2Tr92vlJK2s0w5FXx8fHx8fHx85311Fqu+tmnkVPDx8fHx8fHxHfPF/ZZl2Qqr19fX6UVOBR8fHx8fHx/fOV/993K53HxEzjSq5ORo8PHx8fHx8fGNfZfL5apjVQu0KU7BP2L4q1fx7Q1/HbkhfHx8fHx8fHzfwZcfAYvtsdGZpxwNPj4+Pj4+vp/iix2r1pblVPDx8fHx8fHxnfOt6zp8GjHXXxyt+OK6etGYAxEryPp9nNK/Fx3fm9Ln4+Pj4+Pj43s23zzPU6/oSymll7ZCO5MDESfm2xyI9mL1+2VZdqf0R2eZfHx8fHx8fHzP4htdN8vR4OPj4+Pj4+M752sLq9jZyu0mvccf4xtocyBiKy3mQPTOMmMLL7bZeq2+0RkqHx8fHx8fH99X+3oF3Za/1eLrAjkafHx8fHx8fHx93728q5viSk4FHx8fHx8fH9/xgq5dl4/AYrttdOYpR4OPj4+Pj4/vN/lGBVjuLZCjwcfHx8fHx8c39o0Kq1LKe3ElR4OPj4+Pj4+P79xThj3fNE1pip/mvDf8FXMgeueT7Q357JwKPj4+Pj4+Pr6v8N2b7cpnKjQ5Gnx8fHx8fHx8aVhYpZRSlqPBx8fHx8fHx3feF48XY+HXzbm615rrVWlyNPj4+Pj4+Ph+m69df5PQ3lZz7Ua9ifpRa64HG7X6Rm+gvXF8fHx8fHx8fM/ki8eVcb/cXmy0kRwNPj4+Pj4+Pr7b48q2oMu9s8cWJkeDj4+Pj4+Pj2/sK6VcJ7TL0eDj4+Pj4+PjO+6r+9WCbJ7n/55ujC0yORp8fHx8fHx8fPu+ul/tWNVCbpqm986VHA0+Pj4+Pj4+vuO+dV3TNE1pnuftuluIqJwKPj4+Pj4+Pr7zvnrdtpjbPv5GjgYfHx8fHx8f38d9uW4kR4OPj4+Pj4+P75hvFML+9vZWspwKPj4+Pj4+Pr5zvnYO66oA611MjgYfHx8fHx8f331fm9Bef87ti3I0+Pj4+Pj4+Pj2fbGgK6WkdV3fnx4ctcjkaPDx8fHx8fHxTYeG6+O6q4+/kaPBx8fHx8fHx3fO1657qdVaXRBzIGLaaFsZjs4oR48/9irIekPqazGnou7Dx8fHx8fHx/esvu4QfjyjlFPBx8fHx8fHx5cPHwX2jiqzHA0+Pj4+Pj4+vvO+UQGWewvkaPDx8fHx8fHxnfdtUQxyNPj4+Pj4+Pj4zvl6X8uylKl+qnNKt48rfrTia4fKeq2+djCtHWKLTwfw8fHx8fHx8T2Tr92vlJK2s0w5FXx8fHx8fHx85311Fqu+tmnkVPDx8fHx8fHxHfPF/ZZl2Qqr19fX6UVOBR8fHx8fHx/fOV/993K53HxEzjSq5ORo8PHx8fHx8fGNfZfL5apjVQu0KU7BP2L4q1fx7Q1/HbkhfHx8fHx8fHzfwZcfAYvtsdGZpxwNPj4+Pj6ZIRpyAAAINUlEQVQ+vp/iix2r1pblVPDx8fHx8fHxnfOt6zp8GjHXXxyt+OK6etGYAxEryPp9nNK/Fx3fm9Ln4+Pj4+Pj43s23zzPU6/oSymll7ZCO5MDESfm2xyI9mL1+2VZdqf0R2eZfHx8fHx8fHzP4htdN8vR4OPj4+Pj4+M752sLq9jZyu0mvccf4xtocyBiKy3mQPTOMmMLL7bZeq2+0RkqHx8fHx8fH99X+3oF3Za/1eLrAjkafHx8fHx8fHx93728q5viSk4FHx8fHx8fH9/xgq5dl4/AYrttdOYpR4OPj4+Pj4/vN/lGBVjuLZCjwcfHx8fHx8c39o0Kq1LKe3ElR4OPj4+Pj4+P79xThj3fNE1pip/mvDf8FXMgeueT7Q357JwKPj4+Pj4+Pr6v8N2b7cpnKjQ5Gnx8fHx8fHx8aVhYpZRSlqPBx8fHx8fHx3feF48XY+HXzbm615rrVWlyNPj4+Pj4+Ph+m69df5PQ3lZz7Ua9ifpRa64HG7X6Rm+gvXF8fHx8fHx8fM/ki8eVcb/cXmy0kRwNPj4+Pj4+Pr7b48q2oMu9s8cWJkeDj4+Pj4+Pj2/sK6VcJ7TL0eDj4+Pj4+PjO+6r+9WCbJ7n/55ujC0yORp8fHx8fHx8fPu+ul/tWNVCbpqm986VHA0+Pj4+Pj4+vuO+dV3TNE1pnuftuluIqJwKPj4+Pj4+Pr7zvnrdtpjbPv5GjgYfHx8fHx8f38d9uW4kR4OPj4+Pj4+P75hvFML+9vZWspwKPj4+Pj4+Pr5zvnYO66oA611MjgYfHx8fHx8f331fm9Bef87ti3I0+Pj4+Pj4+Pj2fbGgK6WkdV3fnx4ctcjkaPDx8fHx8fHxTYeG6+O6q4+/kaPBx8fHx8fHx3fO1657qdVaXRBzIGLaaFsZjs4oR48/9irIekPqazGnou7Dx8fHx8fHx/esvu4QfjyjlFPBx8fHx8fHx5cPHwX2jiqzHA0+Pj4+Pj4+vvO+UQGWewvkaPDx8fHx8fHxnfdtUQxyNPj4+Pj4+Pj4zvl6X8uylKl+qnNKt48rfrTia4fKeq2+djCtHWKLTwfw8fHx8fHx8T2Tr92vlJK2s0w5FXx8fHx8fHx85311Fqu+tmnkVPDx8fHx8fHxHfPF/ZZl2Qqr19fX6UVOBR8fHx8fHx/fOV/993K53HxEzjSq5ORo8PHx8fHx8fGNfZfL5apjVQu0KU7BP2L4q1fx7Q1/HbkhfHx8fHx8fHzfwZcfAYvtsdGZpxwNPj4+Pj4+vp/iix2r1pblVPDx8fHx8fHxnfOt6zp8GjHXXxyt+OK6etGYAxEryPp9nNK/Fx3fm9Ln4+Pj4+Pj43s23zzPU6/oSymll7ZCO5MDESfm2xyI9mL1+2VZdqf0R2eZfHx8fHx8fHzP4htdN8vR4OPj4+Pj4+M752sLq9jZyu0mvccf4xtocyBiKy3mQPTOMmMLL7bZeq2+0RkqHx8fHx8fH99X+3oF3Za/1eLrAjkafHx8fHx8fHx93728q5viSk4FHx8fHx8fH9/xgq5dl4/AYrttdOYpR4OPj4+Pj4/vN/lGBVjuLZCjwcfHx8fHx8c39o0Kq1LKe3ElR4OPj4+Pj4+P79xThj3fNE1pip/mvDf8FXMgeueT7Q357JwKPj4+Pj4+Pr6v8N2b7cpnKjQ5Gnx8fHx8fHx8aVhYpZRSlqPBx8fHx8fHx3feF48XY+HXzbm615rrVWlyNPj4+Pj4+Ph+m69df5PQ3lZz7Ua9ifpRa64HG7X6Rm+gvXF8fHx8fHx8fM/ki8eVcb/cXmy0kRwNPj4+Pj4+Pr7b48q2oMu9s8cWJkeDj4+Pj4+Pj2/sK6VcJ7TL0eDj4+Pj4+PjO+6r+9WCbJ7n/55ujC0yORp8fHx8fHx8fPu+ul/tWNVCbpqm986VHA0+Pj4+Pj4+vuO+dV3TNE1pnuftuluIqJwKPj4+Pj4+Pr7zvnrdtpjbPv5GjgYfHx8fHx8f38d9uW4kR4OPj4+Pj4+P75hvFML+9vZWspwKPj4+Pj4+Pr5zvnYO66oA611MjgYfHx8fHx8f331fm9Bef87ti3I0+Pj4+Pj4+Pj2fbGgK6WkdV3fnx4ctcjkaPDx8fHx8fHxTYeG6+O6q4+/kaPBx8fHx8fHx3fO1657qdVaXRBzIGLaaFsZjs4oR48/9irIekPqazGnou7Dx8fHx8fHx/esvu4QfjyjlFPBx8fHx8fHx5cPHwX2jiqzHA0+Pj4+Pj4+vvO+UQGWewvkaPDx8fHx8fHxnfdtUQxyNPj4+Pj4+Pj4zvl6X8uylKl+qnNKt48rfrTia4fKeq2+djCtHWKLTwfw8fHx8fHx8T2Tr92vlJK2s0w5FXx8fHx8fHx85311Fqu+tmnkVPDx8fHx8fHxHfPF/ZZl2Qqr19fX6UVOBR8fHx8fHx/fOV/993K53HxEzjSq5ORo8PHx8fHx8fGNfZfL5apjVQu0KU7BP2L4q1fx7Q1/HbkhfHx8fHx8fHzfwZcfAYvtsdGZpxwNPj4+Pj4+vp/iix2r1pblVPDx8fHx8fHxnfOt6zp8GjHXXxyt+OK6etGYAxEryPp9nNK/Fx3fm9Ln4+Pj4+Pj43s23zzPU6/oSymll7ZCO5MDESfm2xyI9mL1+2VZdqf0R2eZfHx8fHx8fHzP4htdN8vR4OPj4+Pj4+M752sLq9jZyu0mvccf4xtocyBiKy3mQPTOMmMLL7bZeq2+0RkqHx8fHx8fH99X+3oF3Za/1eLrAjkafHx8fHx8fHx93728q5viSk4FHx8fHx8fH9/xgq5d939udIhFzw2M+gAAAABJRU5ErkJggg==); +} + +label.form-control{ + background-color: $rudder-bg-light-gray; + box-shadow: none; + border-left: none; + padding: 8px 12px !important; + + i { + font-weight: normal; + opacity: .8; + font-family: var(--font-mono); + font-size: .9em; + margin-left: 2px; + font-style: normal; + } +} + +label.form-control, +label.input-group-text{ + cursor: pointer; +} + +.card-body > .row > div{ + padding: 15px 30px; +} +.card-header a{ + padding: 10px 15px; + display: flex; + border-bottom: 2px solid #D6DEEF; + background-color: #fff; + font-weight: bold; + transition-duration: 0.2s; + color: #041922 !important; +} +.card:hover .card-header a{ + border-bottom-color: #13beb7; +} +.card-header a.collapsed{ + border-bottom: 2px solid $rudder-border-color-default; +} +/* -- PANEL SEPARATION -- */ +.card-body > .card-col:first-child{ + padding: 15px; + margin-right: -1px; + border-right: 1px solid $rudder-border-color-default; + position: relative; + background-color: #fff; +} +.card-body > .card-col:last-child:before{ + content: ""; + z-index: 100; + position: absolute; + left: -1px; + top: calc(50% - 20px); + width: 0; + height: 0; + border-top: 20px solid transparent; + border-bottom: 20px solid transparent; + border-left: 20px solid #fff; + display: block; +} +.card-body > .card-col:last-child:after{ + content: ""; + z-index: 95; + position: absolute; + left: 0px; + top: calc(50% - 20px); + width: 0; + height: 0; + border-top: 20px solid transparent; + border-bottom: 20px solid transparent; + border-left: 20px solid $rudder-border-color-default; + display: block; +} +.card-body > .card-col:last-child{ + padding: 15px; + border-left: 1px solid $rudder-border-color-default; + background-color: $rudder-bg-light-gray; + display: flex; +} + +@media screen and (min-width: 992px){ + .card-body > .card-col:first-child{ + margin-right: -1px; + border-right: 1px solid $rudder-border-color-default; + } + .card-body > .card-col:last-child:before{ + left: -1px; + top: calc(50% - 20px); + width: 0; + height: 0; + border-top: 20px solid transparent; + border-bottom: 20px solid transparent; + border-left: 20px solid #fff; + } + .card-body > .card-col:last-child:after{ + left: 0px; + top: calc(50% - 20px); + width: 0; + height: 0; + border-top: 20px solid transparent; + border-bottom: 20px solid transparent; + border-left: 20px solid $rudder-border-color-default; + } +} +/* -- --------------- -- */ +.preview-window{ + background-color: #fff; + margin:auto; + border:1px solid $rudder-border-color-default; + width:100%; + max-width: 400px; + position: relative; + + .custom-bar{ + width: 100%; + height:20px; + text-align: center; + position: absolute; + top: 0; + left: 0; + + & > span{ + font-size: 12px; + line-height: 20px; + font-weight: bold; + } + + &.hidden { + & + .top-menu, + & + .left-menu{ + margin-top: 0px; + } + } + } + + & > .left-menu{ + width: 20%; + height:220px; + background-color: #041922; + margin-top: -20px; + } + + & > .top-menu{ + height: 25px; + background-color: #fff; + position: relative; + width: 100%; + margin-top: 20px; + + &:after { + content: ""; + box-shadow: 2px 2px 5px #dce2e4b8; + position: absolute; + display: block; + top: 0; + left: 20%; + right: 0; + bottom: 0; + } + + & > div{ + position: absolute; + left: 0; + top: 0; + height: 25px; + width: 20%; + background-color: #041922; + + & > span{ + position: absolute; + left : 10px; + right : 10px; + top : 5px; + bottom: 5px; + background-size: contain; + background-position: center; + background-repeat: no-repeat; + } + } + } +} + +/* === LOGIN === */ +.preview-window.login{ + padding: 15px; + background-color: $rudder-bg-light-gray; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} +.login .login-container{ + width: 62%; +} +.login .logo-container > div{ + height: 40px; + width: 100%; + background-size: contain; + background-position: center; + background-repeat: no-repeat; + margin: 12px 0 10px 0; +} +.preview-window .fake-form{ + width: 100%; + border-radius: 14px; + padding: 20px; + box-shadow: 0 10px 20px 10px #dce3ef9e; + position: relative; + margin-bottom: 25px; +} +.preview-window .fake-form .custom-bar{ + height:15px; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + font-weight: 700; + border-top-left-radius: 14px; + border-top-right-radius: 14px; +} +.preview-window .fake-form .custom-bar > span{ + line-height: 15px; +} +.preview-window .fake-form .text-motd{ + margin-top: 5px; + padding: 0 8px; + font-size: .9em; + text-align: center; +} +.preview-window .fake-form .fake-input{ + width: 90%; + margin: auto; + height: 20px; + border-radius: 4px; + border:1px solid $rudder-border-color-default; + margin-top: 12px; +} +.preview-window .fake-form .fake-btn{ + background-color: #13beb7; + width: 90%; + margin: auto; + height: 20px; + border-radius: 4px; + margin-top: 12px; +} + +/* LOGOS PREVIEW */ +.preview-logo{ + width: 100%; + max-width: 370px; + display: flex; + margin: auto; + transition-property: opacity; + transition-duration: .2s; + height: 40px; + position: relative; + padding: 15px; + box-sizing: content-box; + + &:before, + &:after{ + content:""; + position: absolute; + top: 0; + bottom: 0; + z-index: 1; + transition-property: opacity; + transition-duration: .2s; + } + &:before{ + left: 0; + width: 70px; + background-color: #041922; + } + &:after{ + left: 70px; + width: calc(100% - 70px); + background-color: #041922; + } + &.sm-disabled:before, + &.lg-disabled:after{ + opacity: .5; + } + + & > div{ + background-position: center; + background-size: contain; + background-repeat: no-repeat; + position: relative; + z-index: 2; + + &:first-child{ + width: 40px; + } + &:last-child{ + flex: 1; + } + } +} + +/* -- COLOR PICKER -- */ +ui-dropdown-panel{ + z-index:9999 !important; +} +.input-group > .input-group-text + ui-picker{ + border-color: $rudder-border-color-default; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + + & > ui-picker-input{ + padding: 0; + height: 100%; + + & > ui-color-picker-rect{ + top: 0; + right: -1px; + width: 34px; + height: 33.5px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + padding: 11px 10px; + background-image: none; + background-color: $rudder-bg-light-gray; + border: 1px solid $rudder-border-color-default; + border-top: none; + border-bottom: none; + } + } +} +.input-group > .input-group-text + ui-picker ui-color-picker-background{ + border-radius: 2px; +} +.input-group > .input-group-text + ui-picker ui-color-picker-text{ + padding: 6px 12px; +} + +#brandingTab .input-group > .input-group-text:last-child > .fa{ + color: #72829D; + transition-duration: .2s; +} +.remove-btn , +.upload-btn { + transition-duration: .2s; +} +#brandingTab .input-group > .input-group-text.remove-btn:hover > i.fa{ + color: #DA291C; +} +#brandingTab .input-group > .input-group-text.upload-btn:hover > i.fa{ + color: #337ab7; +} +#brandingTab .input-group > .input-group-text.remove-btn > i.fa-upload, +#brandingTab .input-group > .input-group-text.upload-btn > i.fa-times{ + display: none; +} + +#brandingTab .toolbar { + background-color: #fff; + border-top: 1px solid $rudder-border-color-default; + position: fixed; + z-index: 200; + text-align: right; + bottom: 0; + left: 0; + width: 100%; + padding: 10px 30px; + .btn{ + min-width: 80px; + } +} + +/* CSS EFFECTS */ +#headerBar > .background{ + transition-duration:.6s; + transition-property: color, background-color; +} \ No newline at end of file From cb6c59b15cdfc387a35395bf256f1ff3699c87ca Mon Sep 17 00:00:00 2001 From: "Francois @fanf42 Armand" Date: Sat, 29 Mar 2025 21:50:41 +0100 Subject: [PATCH 36/65] Fixes #26647: Change validation links are all broken --- change-validation/src/main/resources/change-validation.conf | 6 +++--- .../resources/toserve/changevalidation/change-validation.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/change-validation/src/main/resources/change-validation.conf b/change-validation/src/main/resources/change-validation.conf index 81b1795b1..82d821be6 100644 --- a/change-validation/src/main/resources/change-validation.conf +++ b/change-validation/src/main/resources/change-validation.conf @@ -7,20 +7,20 @@ smtp.password="" # The rudder base URL (domain) as seen from people who will receive emails. This parameter is used # to display link to change requests in notification emails. -# If the CR URL is: https://my.rudder.server/rudder/secure/plugins/changes/changeRequest/1 +# If the CR URL is: https://my.rudder.server/rudder/secure/configurationManager/changes/changeRequest/1 # You should use: https://my.rudder.server/rudder [without end slash] rudder.base.url="https://my.rudder.server/rudder" # The rudder base URL (domain) as seen from people who will receive emails. This parameter is used # to display link to change requests in notification emails. -# If the CR URL is: https://my.rudder.server/rudder/secure/plugins/changes/changeRequest/1 +# If the CR URL is: https://my.rudder.server/rudder/secure/configurationManager/changes/changeRequest/1 # You should use: https://my.rudder.server/rudder [without end slash] rudder.base.url="https://my.rudder.server/rudder" # `subject` parameter support templating. -# Please refer to the documentation : https://docs.rudder.io/reference/6.1/plugins/change-validation.html +# Please refer to the documentation : https://docs.rudder.io/reference/current/plugins/change-validation.html # to know which parameters can be used for templating. # Pending validation diff --git a/change-validation/src/main/resources/toserve/changevalidation/change-validation.js b/change-validation/src/main/resources/toserve/changevalidation/change-validation.js index 158ee3b2a..56e3517b0 100644 --- a/change-validation/src/main/resources/toserve/changevalidation/change-validation.js +++ b/change-validation/src/main/resources/toserve/changevalidation/change-validation.js @@ -57,7 +57,7 @@ function createChangeRequestTable(gridId, data, contextPath, refresh) { $(nTd).empty(); $(nTd).addClass("link"); var editLink = $(""); - editLink.attr("href",contextPath +'/secure/plugins/changes/changeRequest/'+sData); + editLink.attr("href",contextPath +'/secure/configurationManager/changes/changeRequest/'+sData); editLink.text(sData); $(nTd).append(editLink); } @@ -73,7 +73,7 @@ function createChangeRequestTable(gridId, data, contextPath, refresh) { $(nTd).empty(); $(nTd).addClass("link"); var editLink = $(""); - editLink.attr("href",contextPath +'/secure/plugins/changes/changeRequest/'+oData.id); + editLink.attr("href",contextPath +'/secure/configurationManager/changes/changeRequest/'+oData.id); editLink.text(sData); $(nTd).append(editLink); } From 80ec042f6a2aae6dfded913bab2b5710c2d4f6a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Membr=C3=A9?= Date: Thu, 10 Apr 2025 10:20:25 +0200 Subject: [PATCH 37/65] Prepare next nightly branch of plugin --- main-build.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main-build.conf b/main-build.conf index 167b1fef6..96d8263de 100644 --- a/main-build.conf +++ b/main-build.conf @@ -14,6 +14,6 @@ # Version of Rudder used to build the plugin. # It defined the API/ABI used and it is important for binary compatibility branch-type=next -rudder-version=8.3.0~beta2 +rudder-version=8.3.0~rc1 common-version=2.1.1 private-version=2.1.0 From 5ef6a617048cb18acef7d45ce15ac9abd6f9bf3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Membr=C3=A9?= Date: Thu, 10 Apr 2025 10:21:22 +0200 Subject: [PATCH 38/65] Prepare next nightly branch of plugin --- main-build.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main-build.conf b/main-build.conf index 96d8263de..fbf9bdb5c 100644 --- a/main-build.conf +++ b/main-build.conf @@ -14,6 +14,6 @@ # Version of Rudder used to build the plugin. # It defined the API/ABI used and it is important for binary compatibility branch-type=next -rudder-version=8.3.0~rc1 +rudder-version=8.3.0~rc2 common-version=2.1.1 private-version=2.1.0 From d43ddb4fa9b2fc5068080b6b19060f83c6e7df09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Membr=C3=A9?= Date: Thu, 10 Apr 2025 16:21:45 +0200 Subject: [PATCH 39/65] Pass Jenkinsfile release as ns-remap --- Jenkinsfile-release | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Jenkinsfile-release b/Jenkinsfile-release index e059ab575..c0df50d8e 100644 --- a/Jenkinsfile-release +++ b/Jenkinsfile-release @@ -27,11 +27,12 @@ pipeline { // only publish nightly on dev branches agent { dockerfile { + label "generic-docker" filename 'ci/plugins.Dockerfile' additionalBuildArgs "--build-arg USER_ID=${env.JENKINS_UID}" // set same timezone as some tests rely on it // and share maven cache - args '-v /etc/timezone:/etc/timezone:ro -v /srv/cache/elm:/home/jenkins/.elm -v /srv/cache/maven:/home/jenkins/.m2' + args '-u 0:0 -v /etc/timezone:/etc/timezone:ro -v /srv/cache/elm:/root/.elm -v /srv/cache/maven:/home/root/.m2' } } steps { From 89c3283025f8e0423c13b0b5e337440651fac24c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Membr=C3=A9?= Date: Tue, 15 Apr 2025 17:19:35 +0200 Subject: [PATCH 40/65] Update jenkinsfile version --- Jenkinsfile | 2 +- Jenkinsfile-security | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 86ca17c64..9873250b2 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,6 +1,6 @@ def failedBuild = false -def minor_version = "9.0" +def minor_version = "9.1" def version = "${minor_version}" def changeUrl = env.CHANGE_URL def slackResponse = null diff --git a/Jenkinsfile-security b/Jenkinsfile-security index 9cb8b38b0..a26c55b42 100644 --- a/Jenkinsfile-security +++ b/Jenkinsfile-security @@ -1,5 +1,5 @@ -def version = "9.0" +def version = "9.1" def changeUrl = env.CHANGE_URL def job = "" def errors = [] From 736240b3427ea29d4e1d619681dbf1ba83e31864 Mon Sep 17 00:00:00 2001 From: vhayaert Date: Tue, 8 Apr 2025 15:02:20 +0200 Subject: [PATCH 41/65] Fixes #26698: Refactoring of ValidationNeeded class : replace type Box with IOResult --- .../rudder/plugin/ChangeValidationConf.scala | 54 ++++--- .../ChangeRequestJdbcRepository.scala | 87 ++++++------ .../ChangeRequestRepository.scala | 52 ++++--- .../changevalidation/ValidationNeeded.scala | 115 ++++++++------- .../WorkflowJdbcRepository.scala | 98 +++++++------ .../changevalidation/WorkflowService.scala | 111 +++++++-------- .../api/ChangeRequestApi.scala | 58 ++++---- .../comet/WorkflowInformation.scala | 101 ++++++++------ .../snippet/ChangeRequestChangesForm.scala | 21 +-- .../snippet/ChangeRequestDetails.scala | 132 ++++++++++-------- .../snippet/ChangeRequestManagement.scala | 94 +++++++------ .../api_changerequest.yml | 22 +-- .../ChangeRequestJdbcRepositoryTest.scala | 49 ++++--- .../changevalidation/MockServices.scala | 72 +++++----- .../ValidationNeededTest.scala | 16 +-- .../WorkflowJdbcRepositoryTest.scala | 16 +-- .../api/ChangeRequestApiTest.scala | 12 +- 17 files changed, 588 insertions(+), 522 deletions(-) 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 f2f5273a2..c6bddd710 100644 --- a/change-validation/src/main/scala/bootstrap/rudder/plugin/ChangeValidationConf.scala +++ b/change-validation/src/main/scala/bootstrap/rudder/plugin/ChangeValidationConf.scala @@ -41,7 +41,7 @@ import bootstrap.liftweb.RudderConfig import bootstrap.liftweb.RudderConfig.commitAndDeployChangeRequest import bootstrap.liftweb.RudderConfig.doobie import bootstrap.liftweb.RudderConfig.workflowLevelService -import com.normation.box.* +import com.normation.errors.IOResult import com.normation.eventlog.EventActor import com.normation.plugins.PluginStatus import com.normation.plugins.RudderPluginModule @@ -88,8 +88,7 @@ import com.normation.rudder.services.workflows.WorkflowLevelService import com.normation.rudder.services.workflows.WorkflowService import com.normation.zio.UnsafeRun import java.nio.file.Paths -import net.liftweb.common.Box -import net.liftweb.common.Full +import zio.syntax.ToZio /* * The validation workflow level @@ -99,15 +98,15 @@ class ChangeValidationWorkflowLevelService( defaultWorkflowService: WorkflowService, validationWorkflowService: TwoValidationStepsWorkflowServiceImpl, validationNeeded: Seq[ValidationNeeded], - workflowEnabledByUser: () => Box[Boolean], - alwaysNeedValidation: () => Box[Boolean], + workflowEnabledByUser: () => IOResult[Boolean], + alwaysNeedValidation: () => IOResult[Boolean], validatedUserRepo: RoValidatedUserRepository ) extends WorkflowLevelService { override def workflowLevelAllowsEnable: Boolean = status.isEnabled() override def workflowEnabled: Boolean = { - workflowLevelAllowsEnable && workflowEnabledByUser().getOrElse(false) + workflowLevelAllowsEnable && workflowEnabledByUser().orElseSucceed(false).runNow } override def name: String = "Change Request Validation Workflows" @@ -117,7 +116,7 @@ class ChangeValidationWorkflowLevelService( * return the correct workflow given the "needed" check. Also check * for the actual status of workflow to decide what workflow to use. */ - private[this] def getWorkflow(shouldBeNeeded: Box[Boolean]): Box[WorkflowService] = { + private[this] def getWorkflow(shouldBeNeeded: IOResult[Boolean]): IOResult[WorkflowService] = { for { need <- shouldBeNeeded } yield { @@ -133,19 +132,19 @@ class ChangeValidationWorkflowLevelService( * Method to use to combine several validationNeeded check. * Note that a validated user will prevent workflow to be performed, no other validationNeeded check will be executed */ - def combine[T]( - checkFn: (ValidationNeeded, EventActor, T) => Box[Boolean], + private def combine[T]( + checkFn: (ValidationNeeded, EventActor, T) => IOResult[Boolean], checks: Seq[ValidationNeeded], actor: EventActor, change: T - ): Box[WorkflowService] = { + ): IOResult[WorkflowService] = { def getWorkflowAux = { // When we "always need validation", we ignore all validationNeeded checks, otherwise we validate using these checks getWorkflow(validationNeeded.foldLeft(alwaysNeedValidation()) { case (shouldValidate, nextCheck) => shouldValidate.flatMap { // logic is "or": if previous should validate is true, don't check following - case true => Full(true) + case true => true.succeed case false => checkFn(nextCheck, actor, change) } }) @@ -162,47 +161,46 @@ class ChangeValidationWorkflowLevelService( validatedUserRepo .get(actor) .chainError("Could get user from validated user list when checking validation workflow") - .toBox .flatMap { - case Some(e) => getWorkflow(Full(false)) + case Some(e) => getWorkflow(false.succeed) case None => getWorkflowAux } } - override def getForRule(actor: EventActor, change: RuleChangeRequest): Box[WorkflowService] = { + override def getForRule(actor: EventActor, change: RuleChangeRequest): IOResult[WorkflowService] = { combine[RuleChangeRequest]((v, a, c) => v.forRule(a, c), validationNeeded, actor, change) } - override def getForDirective(actor: EventActor, change: DirectiveChangeRequest): Box[WorkflowService] = { + override def getForDirective(actor: EventActor, change: DirectiveChangeRequest): IOResult[WorkflowService] = { combine[DirectiveChangeRequest]((v, a, c) => v.forDirective(a, c), validationNeeded, actor, change) } - override def getForNodeGroup(actor: EventActor, change: NodeGroupChangeRequest): Box[WorkflowService] = { + override def getForNodeGroup(actor: EventActor, change: NodeGroupChangeRequest): IOResult[WorkflowService] = { combine[NodeGroupChangeRequest]((v, a, c) => v.forNodeGroup(a, c), validationNeeded, actor, change) } - override def getForGlobalParam(actor: EventActor, change: GlobalParamChangeRequest): Box[WorkflowService] = { + override def getForGlobalParam(actor: EventActor, change: GlobalParamChangeRequest): IOResult[WorkflowService] = { combine[GlobalParamChangeRequest]((v, a, c) => v.forGlobalParam(a, c), validationNeeded, actor, change) } - override def getByDirective(uid: DirectiveUid, onlyPending: Boolean): Box[Vector[ChangeRequest]] = { + override def getByDirective(uid: DirectiveUid, onlyPending: Boolean): IOResult[Vector[ChangeRequest]] = { if (workflowEnabled) { validationWorkflowService.roChangeRequestRepository.getByDirective(uid, onlyPending) } else { - Full(Vector()) + Vector().succeed } } - override def getByNodeGroup(id: NodeGroupId, onlyPending: Boolean): Box[Vector[ChangeRequest]] = { + override def getByNodeGroup(id: NodeGroupId, onlyPending: Boolean): IOResult[Vector[ChangeRequest]] = { if (workflowEnabled) { validationWorkflowService.roChangeRequestRepository.getByNodeGroup(id, onlyPending) } else { - Full(Vector()) + Vector().succeed } } - override def getByRule(id: RuleUid, onlyPending: Boolean): Box[Vector[ChangeRequest]] = { + override def getByRule(id: RuleUid, onlyPending: Boolean): IOResult[Vector[ChangeRequest]] = { if (workflowEnabled) { validationWorkflowService.roChangeRequestRepository.getByRule(id, onlyPending) } else { - Full(Vector()) + Vector().succeed } } } @@ -244,9 +242,9 @@ object ChangeValidationConf extends RudderPluginModule { woChangeRequestRepository, notificationService, RudderConfig.userService, - () => Full(RudderConfig.workflowLevelService.workflowEnabled), - () => RudderConfig.configService.rudder_workflow_self_validation().toBox, - () => RudderConfig.configService.rudder_workflow_self_deployment().toBox + () => RudderConfig.workflowLevelService.workflowEnabled.succeed, + () => RudderConfig.configService.rudder_workflow_self_validation(), + () => RudderConfig.configService.rudder_workflow_self_deployment() ) lazy val unsupervisedTargetRepo = new UnsupervisedTargetsRepository( @@ -290,8 +288,8 @@ object ChangeValidationConf extends RudderPluginModule { RudderConfig.nodeFactRepository ) ), - () => RudderConfig.configService.rudder_workflow_enabled().toBox, - () => RudderConfig.configService.rudder_workflow_validate_all().toBox, + () => RudderConfig.configService.rudder_workflow_enabled(), + () => RudderConfig.configService.rudder_workflow_validate_all(), roValidatedUserRepository ) ) diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepository.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepository.scala index ad53ab95b..084ce6e3c 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepository.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepository.scala @@ -154,8 +154,8 @@ class RoChangeRequestJdbcRepository( import doobie.* // utility method which correctly transform Doobie types towards Box[Vector[ChangeRequest]] - private[this] def execQuery(q: Query0[Box[ChangeRequest]]): Box[Vector[ChangeRequest]] = { - transactRunBox(xa => { + private[this] def execQuery(errMsg: String, q: Query0[Box[ChangeRequest]]): IOResult[Vector[ChangeRequest]] = { + transactIOResult(errMsg)(xa => { q.to[Vector] .map( // we are just ignoring change request with unserialisation @@ -166,47 +166,52 @@ class RoChangeRequestJdbcRepository( }) } - override def getAll(): Box[Vector[ChangeRequest]] = { - execQuery(getAllSQL) + override def getAll(): IOResult[Vector[ChangeRequest]] = { + execQuery("Could not get all change requests in database", getAllSQL) } - override def get(changeRequestId: ChangeRequestId): Box[Option[ChangeRequest]] = { - transactRunBox(xa => getSQL(changeRequestId).option.map(_.flatMap(_.toOption)).transact(xa)) + override def get(changeRequestId: ChangeRequestId): IOResult[Option[ChangeRequest]] = { + transactIOResult(s"Could not get change request with id ${changeRequestId} in database")(xa => + getSQL(changeRequestId).option.map(_.flatMap(_.toOption)).transact(xa) + ) } // Get every change request where a user add a change - override def getByContributor(actor: EventActor): Box[Vector[ChangeRequest]] = { - execQuery(getByContributorSQL(actor)) + override def getByContributor(actor: EventActor): IOResult[Vector[ChangeRequest]] = { + execQuery(s"Could not get change requests that were modified by ${actor.name} in database", getByContributorSQL(actor)) } - override def getByDirective(id: DirectiveUid, onlyPending: Boolean): Box[Vector[ChangeRequest]] = { + override def getByDirective(id: DirectiveUid, onlyPending: Boolean): IOResult[Vector[ChangeRequest]] = { execQuery( + s"Could not get change requests for directive with id ${id.value} in database", getChangeRequestsByXpathContentSQL( directiveIdXPathFr, id.value, onlyPending ) - ) ?~! s"could not fetch change request for directive with id ${id.value}" + ) } - override def getByNodeGroup(id: NodeGroupId, onlyPending: Boolean): Box[Vector[ChangeRequest]] = { + override def getByNodeGroup(id: NodeGroupId, onlyPending: Boolean): IOResult[Vector[ChangeRequest]] = { execQuery( + s"Could not get change requests for group with id ${id.serialize} in database", getChangeRequestsByXpathContentSQL( groupIdXPathFr, id.serialize, onlyPending ) - ) ?~! s"could not fetch change request for group with id ${id.serialize}" + ) } - override def getByRule(id: RuleUid, onlyPending: Boolean): Box[Vector[ChangeRequest]] = { + override def getByRule(id: RuleUid, onlyPending: Boolean): IOResult[Vector[ChangeRequest]] = { execQuery( + s"Could not get change requests for rule with id ${id.value} in database", getChangeRequestsByXpathContentSQL( ruleIdXPathFr, id.value, onlyPending ) - ) ?~! s"could not fetch change request for rule with id ${id.value}" + ) } override def getByFilter(filter: ChangeRequestFilter): IOResult[Vector[(ChangeRequest, WorkflowNodeId)]] = { @@ -259,19 +264,18 @@ class WoChangeRequestJdbcRepository( * The id is ignored, and a new one will be attributed * to the change request. */ - def createChangeRequest(changeRequest: ChangeRequest, actor: EventActor, reason: Option[String]): Box[ChangeRequest] = { + def createChangeRequest(changeRequest: ChangeRequest, actor: EventActor, reason: Option[String]): IOResult[ChangeRequest] = { val (name, desc, xml, modId) = getAtom(changeRequest) for { - id <- transactRunBox(xa => createChangeRequestSQL(name, desc, xml, modId).withUniqueGeneratedKeys[Int]("id").transact(xa)) - cr <- roRepo.get(ChangeRequestId(id)).flatMap { - case None => - val msg = s"The newly saved change request with ID ${id} was not found back in data base" - ChangeValidationLogger.error(msg) - Failure(msg) - case Some(x) => Full(x) - } + id <- transactIOResult(s"Could not create change request with id ${changeRequest.id} in database")(xa => + createChangeRequestSQL(name, desc, xml, modId).withUniqueGeneratedKeys[Int]("id").transact(xa) + ) + cr <- roRepo + .get(ChangeRequestId(id)) + .notOptional(s"The newly saved change request with id ${id} was not found back in database") + .tapError(err => ChangeValidationLoggerPure.error(err.fullMsg)) } yield { cr } @@ -281,7 +285,11 @@ class WoChangeRequestJdbcRepository( * Delete a change request. * (whatever the read/write mode is). */ - def deleteChangeRequest(changeRequestId: ChangeRequestId, actor: EventActor, reason: Option[String]): Box[ChangeRequest] = { + def deleteChangeRequest( + changeRequestId: ChangeRequestId, + actor: EventActor, + reason: Option[String] + ): IOResult[ChangeRequest] = { // we should update it rather, shouldn't we ? throw new IllegalArgumentException( "This a developer error. Please contact rudder developer, saying that they call unemplemented deleteChangeRequest" @@ -291,29 +299,24 @@ class WoChangeRequestJdbcRepository( /** * Update a change request. The change request must exists. */ - def updateChangeRequest(changeRequest: ChangeRequest, actor: EventActor, reason: Option[String]): Box[ChangeRequest] = { + def updateChangeRequest(changeRequest: ChangeRequest, actor: EventActor, reason: Option[String]): IOResult[ChangeRequest] = { // no transaction between steps, because we don't actually use anything in the existing change request for { - cr <- roRepo.get(changeRequest.id) - ok <- cr match { - case None => - val msg = s"Cannot update non-existent Change Request with id ${changeRequest.id.value}" - ChangeValidationLogger.warn(msg) - Failure(msg) - case Some(x) => Full("ok") - } - update <- { + _ <- roRepo + .get(changeRequest.id) + .notOptional(s"Cannot update non-existent Change Request with id ${changeRequest.id.value}") + .tapError(err => ChangeValidationLoggerPure.error(err.fullMsg)) + _ <- { val (name, desc, xml, modId) = getAtom(changeRequest) - transactRunBox(xa => updateChangeRequestSQL(name, desc, xml, modId, changeRequest.id).run.transact(xa)) + transactIOResult(s"Could not update the change request with id ${changeRequest.id} in database")(xa => + updateChangeRequestSQL(name, desc, xml, modId, changeRequest.id).run.transact(xa) + ) } - updated <- roRepo.get(changeRequest.id).flatMap { - case None => - val msg = s"Couldn't find the updated entry when updating Change Request ${changeRequest.id.value}" - ChangeValidationLogger.error(msg) - Failure(msg) - case Some(x) => Full(x) - } + updated <- roRepo + .get(changeRequest.id) + .notOptional(s"Couldn't find the updated entry when updating Change Request ${changeRequest.id.value}") + .tapError(err => ChangeValidationLoggerPure.error(err.fullMsg)) } yield { updated } diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestRepository.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestRepository.scala index 90aa125ae..aa321fd56 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestRepository.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestRepository.scala @@ -45,24 +45,23 @@ import com.normation.rudder.domain.policies.RuleUid import com.normation.rudder.domain.workflows.ChangeRequest import com.normation.rudder.domain.workflows.ChangeRequestId import com.normation.rudder.domain.workflows.WorkflowNodeId -import net.liftweb.common.Box /** * Read access to change request */ trait RoChangeRequestRepository { - def getAll(): Box[Vector[ChangeRequest]] + def getAll(): IOResult[Vector[ChangeRequest]] - def get(changeRequestId: ChangeRequestId): Box[Option[ChangeRequest]] + def get(changeRequestId: ChangeRequestId): IOResult[Option[ChangeRequest]] - def getByDirective(id: DirectiveUid, onlyPending: Boolean): Box[Vector[ChangeRequest]] + def getByDirective(id: DirectiveUid, onlyPending: Boolean): IOResult[Vector[ChangeRequest]] - def getByNodeGroup(id: NodeGroupId, onlyPending: Boolean): Box[Vector[ChangeRequest]] + def getByNodeGroup(id: NodeGroupId, onlyPending: Boolean): IOResult[Vector[ChangeRequest]] - def getByRule(id: RuleUid, onlyPending: Boolean): Box[Vector[ChangeRequest]] + def getByRule(id: RuleUid, onlyPending: Boolean): IOResult[Vector[ChangeRequest]] - def getByContributor(actor: EventActor): Box[Vector[ChangeRequest]] + def getByContributor(actor: EventActor): IOResult[Vector[ChangeRequest]] def getByFilter(filter: ChangeRequestFilter): IOResult[Vector[(ChangeRequest, WorkflowNodeId)]] @@ -72,35 +71,30 @@ trait RoChangeRequestRepository { * A proxy implementation simply delegating to either A or B implementation */ class EitherRoChangeRequestRepository( - cond: () => Box[Boolean], + cond: () => IOResult[Boolean], whenTrue: RoChangeRequestRepository, whenFalse: RoChangeRequestRepository ) extends RoChangeRequestRepository { - // remove some boilerplate to make following proxy implementation more readable, just exposing the actual method to call - private[this] def condApply[T](method: RoChangeRequestRepository => Box[T]): Box[T] = { - cond().flatMap(if (_) method(whenTrue) else method(whenFalse)) - } - private[this] def condApply[T](method: RoChangeRequestRepository => IOResult[T]): IOResult[T] = { - cond().toIO.flatMap(if (_) method(whenTrue) else method(whenFalse)) + cond().flatMap(if (_) method(whenTrue) else method(whenFalse)) } - def getAll(): Box[Vector[ChangeRequest]] = condApply(_.getAll()) + def getAll(): IOResult[Vector[ChangeRequest]] = condApply(_.getAll()) - def get(changeRequestId: ChangeRequestId): Box[Option[ChangeRequest]] = condApply(_.get(changeRequestId)) + def get(changeRequestId: ChangeRequestId): IOResult[Option[ChangeRequest]] = condApply(_.get(changeRequestId)) - def getByDirective(id: DirectiveUid, onlyPending: Boolean): Box[Vector[ChangeRequest]] = condApply( + def getByDirective(id: DirectiveUid, onlyPending: Boolean): IOResult[Vector[ChangeRequest]] = condApply( _.getByDirective(id, onlyPending) ) - def getByNodeGroup(id: NodeGroupId, onlyPending: Boolean): Box[Vector[ChangeRequest]] = condApply( + def getByNodeGroup(id: NodeGroupId, onlyPending: Boolean): IOResult[Vector[ChangeRequest]] = condApply( _.getByNodeGroup(id, onlyPending) ) - def getByRule(id: RuleUid, onlyPending: Boolean): Box[Vector[ChangeRequest]] = condApply(_.getByRule(id, onlyPending)) + def getByRule(id: RuleUid, onlyPending: Boolean): IOResult[Vector[ChangeRequest]] = condApply(_.getByRule(id, onlyPending)) - def getByContributor(actor: EventActor): Box[Vector[ChangeRequest]] = condApply(_.getByContributor(actor)) + def getByContributor(actor: EventActor): IOResult[Vector[ChangeRequest]] = condApply(_.getByContributor(actor)) def getByFilter(filter: ChangeRequestFilter): IOResult[Vector[(ChangeRequest, WorkflowNodeId)]] = condApply( _.getByFilter(filter) @@ -117,7 +111,7 @@ trait WoChangeRequestRepository { * The id is ignored, and a new one will be attributed * to the change request. */ - def createChangeRequest(changeRequest: ChangeRequest, actor: EventActor, reason: Option[String]): Box[ChangeRequest] + def createChangeRequest(changeRequest: ChangeRequest, actor: EventActor, reason: Option[String]): IOResult[ChangeRequest] /** * Update a change request. The change request must not @@ -126,13 +120,13 @@ trait WoChangeRequestRepository { * will be ignore), an explicit call to setWriteOnly must be * done for that. */ - def updateChangeRequest(changeRequest: ChangeRequest, actor: EventActor, reason: Option[String]): Box[ChangeRequest] + def updateChangeRequest(changeRequest: ChangeRequest, actor: EventActor, reason: Option[String]): IOResult[ChangeRequest] /** * Delete a change request. * (whatever the read/write mode is). */ - def deleteChangeRequest(changeRequestId: ChangeRequestId, actor: EventActor, reason: Option[String]): Box[ChangeRequest] + def deleteChangeRequest(changeRequestId: ChangeRequestId, actor: EventActor, reason: Option[String]): IOResult[ChangeRequest] } @@ -140,23 +134,27 @@ trait WoChangeRequestRepository { * Again, a proxy forwarding to an implementation based on a runtime property */ class EitherWoChangeRequestRepository( - cond: () => Box[Boolean], + cond: () => IOResult[Boolean], whenTrue: WoChangeRequestRepository, whenFalse: WoChangeRequestRepository ) extends WoChangeRequestRepository { - def createChangeRequest(changeRequest: ChangeRequest, actor: EventActor, reason: Option[String]): Box[ChangeRequest] = { + def createChangeRequest(changeRequest: ChangeRequest, actor: EventActor, reason: Option[String]): IOResult[ChangeRequest] = { cond().flatMap( if (_) whenTrue.createChangeRequest(changeRequest, actor, reason) else whenFalse.createChangeRequest(changeRequest, actor, reason) ) } - def updateChangeRequest(changeRequest: ChangeRequest, actor: EventActor, reason: Option[String]): Box[ChangeRequest] = { + def updateChangeRequest(changeRequest: ChangeRequest, actor: EventActor, reason: Option[String]): IOResult[ChangeRequest] = { cond().flatMap( if (_) whenTrue.updateChangeRequest(changeRequest, actor, reason) else whenFalse.updateChangeRequest(changeRequest, actor, reason) ) } - def deleteChangeRequest(changeRequestId: ChangeRequestId, actor: EventActor, reason: Option[String]): Box[ChangeRequest] = { + def deleteChangeRequest( + changeRequestId: ChangeRequestId, + actor: EventActor, + reason: Option[String] + ): IOResult[ChangeRequest] = { cond().flatMap( if (_) whenTrue.deleteChangeRequest(changeRequestId, actor, reason) else whenFalse.deleteChangeRequest(changeRequestId, actor, reason) 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 3e32e9f00..8b96c8cd9 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 @@ -37,7 +37,6 @@ package com.normation.plugins.changevalidation -import com.normation.box.* import com.normation.errors.IOResult import com.normation.eventlog.EventActor import com.normation.inventory.domain.NodeId @@ -52,9 +51,9 @@ 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 net.liftweb.common.Box -import net.liftweb.common.Full import scala.collection.MapView +import zio.ZIO +import zio.syntax.ToZio object bddMock { val USER_AUTH_NEEDED = Map( @@ -71,10 +70,10 @@ object bddMock { * (see https://issues.rudder.io/issues/22188#note-5) */ trait ValidationNeeded { - def forRule(actor: EventActor, change: RuleChangeRequest): Box[Boolean] - def forDirective(actor: EventActor, change: DirectiveChangeRequest): Box[Boolean] - def forNodeGroup(actor: EventActor, change: NodeGroupChangeRequest): Box[Boolean] - def forGlobalParam(actor: EventActor, change: GlobalParamChangeRequest): Box[Boolean] + def forRule(actor: EventActor, change: RuleChangeRequest): IOResult[Boolean] + def forDirective(actor: EventActor, change: DirectiveChangeRequest): IOResult[Boolean] + def forNodeGroup(actor: EventActor, change: NodeGroupChangeRequest): IOResult[Boolean] + def forGlobalParam(actor: EventActor, change: GlobalParamChangeRequest): IOResult[Boolean] } /* @@ -109,22 +108,25 @@ class NodeGroupValidationNeeded( * - rule R is changed to add Group2 in its target (or opposite change: Group2 removed) * - the change must be validated. */ - override def forRule(actor: EventActor, change: RuleChangeRequest): Box[Boolean] = { - val start = System.currentTimeMillis() - val res = (for { + override def forRule(actor: EventActor, change: RuleChangeRequest): IOResult[Boolean] = { + for { + start <- com.normation.zio.currentTimeMillis groups <- groupLib.getFullGroupLibrary() // I think it's ok to have that, it will need a deeper change when we will want to have per-tenant change validation arePolicyServer <- nodeFactRepo.getAll()(QueryContext.systemQC) supervised <- supervisedTargets() + targets = Set(change.newRule) ++ change.previousRule.toSet + res = checkNodeTargetByRule(groups, arePolicyServer.mapValues(_.rudderSettings.isPolicyServer), supervised, targets) + end <- com.normation.zio.currentTimeMillis + _ <- { + ChangeValidationLoggerPure.Metrics.debug( + s"Check rule '${change.newRule.name}' [${change.newRule.id.serialize}]" + + s"change requestion need for validation in ${end - start}ms" + ) + } } yield { - val targets = Set(change.newRule) ++ change.previousRule.toSet - checkNodeTargetByRule(groups, arePolicyServer.mapValues(_.rudderSettings.isPolicyServer), supervised, targets) - }).toBox - ChangeValidationLogger.Metrics.debug( - s"Check rule '${change.newRule.name}' [${change.newRule.id.serialize}] change requestion need for validation in ${System - .currentTimeMillis() - start}ms" - ) - res + res + } } /** @@ -161,7 +163,7 @@ class NodeGroupValidationNeeded( * belong to an other group) * - now the rule is applied to supervised node, but no validation was done. */ - override def forNodeGroup(actor: EventActor, change: NodeGroupChangeRequest): Box[Boolean] = { + override def forNodeGroup(actor: EventActor, change: NodeGroupChangeRequest): IOResult[Boolean] = { // Here we need to test the future content of the group, and not the current one. // So we need to know: // - the list of supervised node in the group before the change, @@ -169,61 +171,70 @@ class NodeGroupValidationNeeded( // - the list of supervised node in the group after change, // => non empty means validation needed - val start = System.currentTimeMillis() - - val res = (for { + for { + start <- com.normation.zio.currentTimeMillis groups <- groupLib.getFullGroupLibrary() nodeFacts <- nodeFactRepo.getAll()(QueryContext.systemQC) supervised <- supervisedTargets() - } yield { - val targetNodes = change.newGroup.serverList ++ change.previousGroup.map(_.serverList).getOrElse(Set()) - val exists = groups - .getNodeIds(supervised.map(identity), nodeFacts.mapValues(_.rudderSettings.isPolicyServer)) - .find(nodeId => targetNodes.contains(nodeId)) + targetNodes = change.newGroup.serverList ++ change.previousGroup.map(_.serverList).getOrElse(Set()) + exists = groups + .getNodeIds(supervised.map(identity), nodeFacts.mapValues(_.rudderSettings.isPolicyServer)) + .find(nodeId => targetNodes.contains(nodeId)) + res <- + // we want to let the log know why the change request needs validation + ZIO + .foreach(exists) { nodeId => + ChangeValidationLoggerPure + .debug( + s"Node '${nodeId.value}' belongs to both a supervised group and to group '${change.newGroup.name}' [${change.newGroup.id.serialize}]" + ) + } + .map(_.nonEmpty) - // we want to let the log knows why the change request need validation - exists.foreach { nodeId => - ChangeValidationLogger.debug( - s"Node '${nodeId.value}' belongs to both a supervised group and to group '${change.newGroup.name}' [${change.newGroup.id.serialize}]" + end <- com.normation.zio.currentTimeMillis + _ <- { + ChangeValidationLoggerPure.Metrics.debug( + s"Check group '${change.newGroup.name}' [${change.newGroup.id.serialize}] " + + s"change requestion need for validation in ${end - start}ms" ) } - exists.nonEmpty - }).toBox - ChangeValidationLogger.Metrics.debug( - s"Check group '${change.newGroup.name}' [${change.newGroup.id.serialize}] change requestion need for validation in ${System - .currentTimeMillis() - start}ms" - ) - res + } yield { + res + } } /* * A directive need a validation if any rule using it need a validation. */ - override def forDirective(actor: EventActor, change: DirectiveChangeRequest): Box[Boolean] = { - // in a change, the old directive id and the new one is the same. - val directiveId = change.newDirective.id - val start = System.currentTimeMillis() - val res = (for { + override def forDirective(actor: EventActor, change: DirectiveChangeRequest): IOResult[Boolean] = { + for { + start <- com.normation.zio.currentTimeMillis + // in a change, the old directive id and the new one is the same. + directiveId = change.newDirective.id rules <- ruleLib.getAll(includeSytem = true).map(_.filter(r => r.directiveIds.contains(directiveId))) // we need to add potentially new rules applied to that directive that the previous request does not cover newRules = change.updatedRules supervised <- supervisedTargets() groups <- groupLib.getFullGroupLibrary() nodeFacts <- nodeFactRepo.getAll()(QueryContext.systemQC) + res = + checkNodeTargetByRule(groups, nodeFacts.mapValues(_.rudderSettings.isPolicyServer), supervised, (rules ++ newRules).toSet) + end <- com.normation.zio.currentTimeMillis + _ <- { + ChangeValidationLoggerPure.Metrics.debug( + s"Check directive '${change.newDirective.name}' [${change.newDirective.id.uid.serialize}]" + + s"change requestion need for validation in ${end - start}ms" + ) + } } yield { - checkNodeTargetByRule(groups, nodeFacts.mapValues(_.rudderSettings.isPolicyServer), supervised, (rules ++ newRules).toSet) - }).toBox - ChangeValidationLogger.Metrics.debug( - s"Check directive '${change.newDirective.name}' [${change.newDirective.id.uid.serialize}] change requestion need for validation in ${System - .currentTimeMillis() - start}ms" - ) - res + res + } } /* * For a global parameter, we just answer "yes" */ - override def forGlobalParam(actor: EventActor, change: GlobalParamChangeRequest): Box[Boolean] = { - Full(true) + override def forGlobalParam(actor: EventActor, change: GlobalParamChangeRequest): IOResult[Boolean] = { + true.succeed } } diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/WorkflowJdbcRepository.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/WorkflowJdbcRepository.scala index 14ceedecc..4562a2b29 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/WorkflowJdbcRepository.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/WorkflowJdbcRepository.scala @@ -37,12 +37,12 @@ package com.normation.plugins.changevalidation import cats.implicits.* +import com.normation.errors.IOResult import com.normation.rudder.db.Doobie import com.normation.rudder.domain.workflows.ChangeRequestId import com.normation.rudder.domain.workflows.WorkflowNodeId import doobie.* import doobie.implicits.* -import net.liftweb.common.Box import net.liftweb.common.Loggable import zio.interop.catz.* @@ -50,17 +50,17 @@ import zio.interop.catz.* * Repository to manage the Workflow part */ trait RoWorkflowRepository { - def getAllByState(state: WorkflowNodeId): Box[Seq[ChangeRequestId]] + def getAllByState(state: WorkflowNodeId): IOResult[Seq[ChangeRequestId]] - def getStateOfChangeRequest(crId: ChangeRequestId): Box[WorkflowNodeId] + def getStateOfChangeRequest(crId: ChangeRequestId): IOResult[WorkflowNodeId] - def getAllChangeRequestsState(): Box[Map[ChangeRequestId, WorkflowNodeId]] + def getAllChangeRequestsState(): IOResult[Map[ChangeRequestId, WorkflowNodeId]] } trait WoWorkflowRepository { - def createWorkflow(crId: ChangeRequestId, state: WorkflowNodeId): Box[WorkflowNodeId] + def createWorkflow(crId: ChangeRequestId, state: WorkflowNodeId): IOResult[WorkflowNodeId] - def updateState(crId: ChangeRequestId, from: WorkflowNodeId, state: WorkflowNodeId): Box[WorkflowNodeId] + def updateState(crId: ChangeRequestId, from: WorkflowNodeId, state: WorkflowNodeId): IOResult[WorkflowNodeId] } trait RoWorkflowJdbcRepositorySQL { @@ -92,16 +92,20 @@ class RoWorkflowJdbcRepository(doobie: Doobie) extends RoWorkflowRepository with import WorkflowJdbcRepositorySQL.* import doobie.* - def getAllByState(state: WorkflowNodeId): Box[Seq[ChangeRequestId]] = { - transactRunBox(xa => getAllByStateSQL(state).to[Vector].transact(xa)) + def getAllByState(state: WorkflowNodeId): IOResult[Seq[ChangeRequestId]] = { + transactIOResult(s"Could not get change request with state ${state.value}")(xa => + getAllByStateSQL(state).to[Vector].transact(xa) + ) } - def getStateOfChangeRequest(crId: ChangeRequestId): Box[WorkflowNodeId] = { - transactRunBox(xa => getStateOfChangeRequestSQL(crId).unique.transact(xa)) + def getStateOfChangeRequest(crId: ChangeRequestId): IOResult[WorkflowNodeId] = { + transactIOResult(s"Could not get state of change request with id ${crId.value}")(xa => + getStateOfChangeRequestSQL(crId).unique.transact(xa) + ) } - def getAllChangeRequestsState(): Box[Map[ChangeRequestId, WorkflowNodeId]] = { - transactRunBox(xa => getAllChangeRequestsStateSQL.to[Vector].transact(xa)) + def getAllChangeRequestsState(): IOResult[Map[ChangeRequestId, WorkflowNodeId]] = { + transactIOResult("Could not get states of all change requests")(xa => getAllChangeRequestsStateSQL.to[Vector].transact(xa)) .map(_.toMap) } } @@ -110,53 +114,55 @@ class WoWorkflowJdbcRepository(doobie: Doobie) extends WoWorkflowRepository with import WorkflowJdbcRepositorySQL.* import doobie.* - def createWorkflow(crId: ChangeRequestId, state: WorkflowNodeId): Box[WorkflowNodeId] = { + def createWorkflow(crId: ChangeRequestId, state: WorkflowNodeId): IOResult[WorkflowNodeId] = { val process = { for { - exists <- getStateOfChangeRequestSQL(crId).option - created <- exists match { - case None => createWorkflowSQL(crId, state).run.attempt - case Some(s) => - val msg = - s"Cannot start a workflow for Change Request id ${crId.value}, as it is already part of a workflow in state '${s}'" - ChangeValidationLogger.error(msg) - (Left(msg)).pure[ConnectionIO] - } + exists <- getStateOfChangeRequestSQL(crId).option + _ <- exists match { + case None => createWorkflowSQL(crId, state).run.attempt + case Some(s) => + val msg = + s"Cannot start a workflow for Change Request id ${crId.value}, as it is already part of a workflow in state '${s}'" + ChangeValidationLogger.error(msg) + (Left(msg)).pure[ConnectionIO] + } } yield { state } } - transactRunBox(xa => process.transact(xa)) + transactIOResult(s"Could not create workflow with id ${crId.value} and state ${state.value}")(xa => process.transact(xa)) } - def updateState(crId: ChangeRequestId, from: WorkflowNodeId, state: WorkflowNodeId): Box[WorkflowNodeId] = { + def updateState(crId: ChangeRequestId, from: WorkflowNodeId, state: WorkflowNodeId): IOResult[WorkflowNodeId] = { val process = { for { - exists <- getStateOfChangeRequestSQL(crId).option - created <- exists match { - case Some(s) => - if (s == from) { - updateStateSQL( - crId, - from, - state - ).run.attempt // swallows any "constraint violation" error if CrId is not in the ChangeRequest table - } else { - val msg = s"Cannot change status of ChangeRequest '${crId.value}': it has the status '${s.value}' " + - s"but we were expecting '${from.value}'. Perhaps someone else changed it concurrently?" - ChangeValidationLogger.error(msg) - (Left(msg).pure[ConnectionIO]) - } - case None => - val msg = - s"Cannot change a workflow for Change Request id ${crId.value}, as it is not part of any workflow yet" - ChangeValidationLogger.error(msg) - (Left(msg).pure[ConnectionIO]) - } + exists <- getStateOfChangeRequestSQL(crId).option + _ <- exists match { + case Some(s) => + if (s == from) { + updateStateSQL( + crId, + from, + state + ).run.attempt // swallows any "constraint violation" error if CrId is not in the ChangeRequest table + } else { + val msg = s"Cannot change status of ChangeRequest '${crId.value}': it has the status '${s.value}' " + + s"but we were expecting '${from.value}'. Perhaps someone else changed it concurrently?" + ChangeValidationLogger.error(msg) + Left(msg).pure[ConnectionIO] + } + case None => + val msg = + s"Cannot change a workflow for Change Request id ${crId.value}, as it is not part of any workflow yet" + ChangeValidationLogger.error(msg) + Left(msg).pure[ConnectionIO] + } } yield { state } } - transactRunBox(xa => process.transact(xa)) + transactIOResult( + s"Could not update state of change request with id ${crId.value} from ${from.value} to ${state.value}" + )(xa => process.transact(xa)) } } diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/WorkflowService.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/WorkflowService.scala index 952384212..a83bec735 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/WorkflowService.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/WorkflowService.scala @@ -37,7 +37,6 @@ package com.normation.plugins.changevalidation -import com.normation.box.* import com.normation.errors.* import com.normation.eventlog.EventActor import com.normation.eventlog.ModificationId @@ -60,29 +59,30 @@ import com.normation.rudder.services.workflows.WorkflowService import com.normation.rudder.services.workflows.WorkflowUpdate import com.normation.rudder.users.UserService import com.normation.utils.StringUuidGenerator -import net.liftweb.common.* +import com.normation.zio.UnsafeRun import org.joda.time.DateTime import zio.* +import zio.syntax.ToZio /** * A proxy workflow service based on a runtime choice */ -class EitherWorkflowService(cond: () => Box[Boolean], whenTrue: WorkflowService, whenFalse: WorkflowService) +class EitherWorkflowService(cond: () => IOResult[Boolean], whenTrue: WorkflowService, whenFalse: WorkflowService) extends WorkflowService { // TODO: handle ERRORS for config! val name = "choose-active-validation-workflow" - def current: WorkflowService = if (cond().getOrElse(false)) whenTrue else whenFalse + def current: WorkflowService = if (cond().orElseSucceed(false).runNow) whenTrue else whenFalse - override def startWorkflow(changeRequest: ChangeRequest)(implicit cc: ChangeContext): Box[ChangeRequestId] = + override def startWorkflow(changeRequest: ChangeRequest)(implicit cc: ChangeContext): IOResult[ChangeRequestId] = current.startWorkflow(changeRequest) - override def openSteps: List[WorkflowNodeId] = + override def openSteps: List[WorkflowNodeId] = current.openSteps - override def closedSteps: List[WorkflowNodeId] = + override def closedSteps: List[WorkflowNodeId] = current.closedSteps - override def stepsValue: List[WorkflowNodeId] = + override def stepsValue: List[WorkflowNodeId] = current.stepsValue override def findNextSteps(currentUserRights: Seq[String], currentStep: WorkflowNodeId, isCreator: Boolean)(implicit qc: QueryContext @@ -92,17 +92,17 @@ class EitherWorkflowService(cond: () => Box[Boolean], whenTrue: WorkflowService, currentUserRights: Seq[String], currentStep: WorkflowNodeId, isCreator: Boolean - ): Seq[(WorkflowNodeId, (ChangeRequestId, EventActor, Option[String]) => Box[WorkflowNodeId])] = + ): Seq[(WorkflowNodeId, (ChangeRequestId, EventActor, Option[String]) => IOResult[WorkflowNodeId])] = current.findBackSteps(currentUserRights, currentStep, isCreator) - override def findStep(changeRequestId: ChangeRequestId): Box[WorkflowNodeId] = + override def findStep(changeRequestId: ChangeRequestId): IOResult[WorkflowNodeId] = current.findStep(changeRequestId) - override def getAllChangeRequestsStep(): Box[Map[ChangeRequestId, WorkflowNodeId]] = + override def getAllChangeRequestsStep(): IOResult[Map[ChangeRequestId, WorkflowNodeId]] = current.getAllChangeRequestsStep() - override def isEditable(currentUserRights: Seq[String], currentStep: WorkflowNodeId, isCreator: Boolean): Boolean = + override def isEditable(currentUserRights: Seq[String], currentStep: WorkflowNodeId, isCreator: Boolean): Boolean = current.isEditable(currentUserRights, currentStep, isCreator) - override def isPending(currentStep: WorkflowNodeId): Boolean = + override def isPending(currentStep: WorkflowNodeId): Boolean = current.isPending(currentStep) - override def needExternalValidation(): Boolean = current.needExternalValidation() + override def needExternalValidation(): Boolean = current.needExternalValidation() } object TwoValidationStepsWorkflowServiceImpl { @@ -137,15 +137,15 @@ class TwoValidationStepsWorkflowServiceImpl( woChangeRequestRepository: WoChangeRequestRepository, notificationService: NotificationService, userService: UserService, - workflowEnable: () => Box[Boolean], - selfValidation: () => Box[Boolean], - selfDeployment: () => Box[Boolean] + workflowEnable: () => IOResult[Boolean], + selfValidation: () => IOResult[Boolean], + selfDeployment: () => IOResult[Boolean] ) extends WorkflowService { import TwoValidationStepsWorkflowServiceImpl.* val name = "two-steps-validation-workflow" - def getItemsInStep(stepId: WorkflowNodeId): Box[Seq[ChangeRequestId]] = roWorkflowRepo.getAllByState(stepId) + def getItemsInStep(stepId: WorkflowNodeId): IOResult[Seq[ChangeRequestId]] = roWorkflowRepo.getAllByState(stepId) val closedSteps: List[WorkflowNodeId] = List(Cancelled.id, Deployed.id) val openSteps: List[WorkflowNodeId] = List(Validation.id, Deployment.id) @@ -166,14 +166,15 @@ class TwoValidationStepsWorkflowServiceImpl( } for { - saved <- save ?~! s"could not save change request ${changeRequest.info.name}" + saved <- save.chainError(s"could not save change request ${changeRequest.info.name}") modId = ModificationId(uuidGen.newUuid) workflowEnable <- workflowEnable() _ <- if (workflowEnable) { - changeRequestEventLogService.saveChangeRequestLog(modId, actor, saved, reason) ?~! - s"could not save event log for change request ${saved.changeRequest.id} creation" + changeRequestEventLogService + .saveChangeRequestLog(modId, actor, saved, reason) + .chainError(s"could not save event log for change request ${saved.changeRequest.id} creation") } else { - Full("OK, no workflow") + "OK, no workflow".succeed } } yield { saved.changeRequest } } @@ -183,7 +184,7 @@ class TwoValidationStepsWorkflowServiceImpl( newInfo: ChangeRequestInfo, actor: EventActor, reason: Option[String] - ): Box[ChangeRequest] = { + ): IOResult[ChangeRequest] = { val newCr = ChangeRequest.updateInfo(oldChangeRequest, newInfo) saveAndLogChangeRequest(ModifyToChangeRequestDiff(newCr, oldChangeRequest), actor, reason) } @@ -198,7 +199,7 @@ class TwoValidationStepsWorkflowServiceImpl( isCreator: Boolean )(implicit qc: QueryContext): WorkflowAction = { - def deployAction(action: (ChangeRequestId, EventActor, Option[String]) => Box[WorkflowNodeId]) = { + def deployAction(action: (ChangeRequestId, EventActor, Option[String]) => IOResult[WorkflowNodeId]) = { if (canDeploy(isCreator, selfDeployment)) Seq((Deployed.id, action)) else Seq() @@ -230,7 +231,7 @@ class TwoValidationStepsWorkflowServiceImpl( currentUserRights: Seq[String], currentStep: WorkflowNodeId, isCreator: Boolean - ): Seq[(WorkflowNodeId, (ChangeRequestId, EventActor, Option[String]) => Box[WorkflowNodeId])] = { + ): Seq[(WorkflowNodeId, (ChangeRequestId, EventActor, Option[String]) => IOResult[WorkflowNodeId])] = { currentStep match { case Validation.id => if (canValidate(isCreator, selfValidation)) @@ -266,7 +267,7 @@ class TwoValidationStepsWorkflowServiceImpl( } } - def isPending(currentStep: WorkflowNodeId): Boolean = { + def isPending(currentStep: WorkflowNodeId): Boolean = { currentStep match { case Validation.id => true case Deployment.id => true @@ -279,15 +280,15 @@ class TwoValidationStepsWorkflowServiceImpl( false } } - def findStep(changeRequestId: ChangeRequestId): Box[WorkflowNodeId] = { + def findStep(changeRequestId: ChangeRequestId): IOResult[WorkflowNodeId] = { roWorkflowRepo.getStateOfChangeRequest(changeRequestId) } - def getAllChangeRequestsStep(): Box[Map[ChangeRequestId, WorkflowNodeId]] = { + def getAllChangeRequestsStep(): IOResult[Map[ChangeRequestId, WorkflowNodeId]] = { roWorkflowRepo.getAllChangeRequestsState() } - def startWorkflow(changeRequest: ChangeRequest)(implicit cc: ChangeContext): Box[ChangeRequestId] = { + def startWorkflow(changeRequest: ChangeRequest)(implicit cc: ChangeContext): IOResult[ChangeRequestId] = { ChangeValidationLogger.debug(s"${name}: start workflow for change request '${changeRequest.id.value}'") for { saved <- saveAndLogChangeRequest(AddChangeRequestDiff(changeRequest), cc.actor, cc.message) @@ -305,7 +306,7 @@ class TwoValidationStepsWorkflowServiceImpl( changeRequestId: ChangeRequestId, actor: EventActor, reason: Option[String] - ): Box[WorkflowNodeId] = { + ): IOResult[WorkflowNodeId] = { (for { state <- woWorkflowRepo.updateState(changeRequestId, from.id, to.id) workflowStep = WorkflowStepChange(changeRequestId, from.id, to.id) @@ -314,13 +315,10 @@ class TwoValidationStepsWorkflowServiceImpl( } yield { workflowComet ! WorkflowUpdate state - }) match { - case Full(state) => Full(state) - case eb: EmptyBox => - val e = eb ?~! s"Error when changing step in workflow for Change Request ${changeRequestId.value}" - ChangeValidationLogger.error(e.messageChain) - e - } + }) + .chainError(s"Error when changing step in workflow for Change Request ${changeRequestId.value}") + .tapError(err => ChangeValidationLoggerPure.error(err.fullMsg)) + } /* @@ -329,13 +327,13 @@ class TwoValidationStepsWorkflowServiceImpl( * - check for current user rights. * WARNING: WE ARE SIDE STEPPING authz check until https://issues.rudder.io/issues/22595 is solved */ - private def canValidate(isCreator: Boolean, selfValidation: () => Box[Boolean]): Boolean = { - val correctActor = selfValidation().getOrElse(false) || !isCreator + private def canValidate(isCreator: Boolean, selfValidation: () => IOResult[Boolean]): Boolean = { + val correctActor = selfValidation().orElseSucceed(false).runNow || !isCreator correctActor && userService.getCurrentUser.checkRights(AuthorizationType.Validator.Edit) } - private def canDeploy(isCreator: Boolean, selfDeployment: () => Box[Boolean]): Boolean = { - val correctActor = selfDeployment().getOrElse(false) || !isCreator + private def canDeploy(isCreator: Boolean, selfDeployment: () => IOResult[Boolean]): Boolean = { + val correctActor = selfDeployment().orElseSucceed(false).runNow || !isCreator correctActor && userService.getCurrentUser.checkRights(AuthorizationType.Deployer.Edit) } @@ -347,7 +345,6 @@ class TwoValidationStepsWorkflowServiceImpl( for { cr <- roChangeRequestRepository .get(changeRequestId) - .toIO .notOptional( s"Change request with ID '${changeRequestId.value}' was not found in database" ) @@ -372,13 +369,10 @@ class TwoValidationStepsWorkflowServiceImpl( */ implicit class CatchEmailError(result: IOResult[Unit]) { def catchEmailError(from: String, to: String): Unit = { - result.toBox match { - case eb: EmptyBox => - val msg = - (eb ?~! s"Error when trying to send email for change request status update from '${from}' to '${to}'").messageChain - ChangeValidationLogger.error(msg) - case Full(_) => // nothing - } + result + .chainError(s"Error when trying to send email for change request status update from '${from}' to '${to}'") + .catchAll(err => ChangeValidationLoggerPure.error(err.fullMsg)) + .runNow } } @@ -389,11 +383,12 @@ class TwoValidationStepsWorkflowServiceImpl( reason: Option[String] )(implicit qc: QueryContext - ): Box[WorkflowNodeId] = { + ): IOResult[WorkflowNodeId] = { ChangeValidationLogger.debug(s"${name}: deploy change for change request '${changeRequestId.value}'") for { - optcr <- roChangeRequestRepository.get(changeRequestId) - cr <- Box(optcr) ?~! s"Change request with ID '${changeRequestId.value}' was not found in database" + cr <- roChangeRequestRepository + .get(changeRequestId) + .notOptional(s"Change request with ID '${changeRequestId.value}' was not found in database") saved <- commit.save(cr)(ChangeContext(ModificationId(uuidGen.newUuid), qc.actor, new DateTime(), reason, None, qc.nodePerms)) _ <- woChangeRequestRepository.updateChangeRequest(saved, actor, reason) @@ -408,7 +403,7 @@ class TwoValidationStepsWorkflowServiceImpl( changeRequestId: ChangeRequestId, actor: EventActor, reason: Option[String] - ): Box[WorkflowNodeId] = { + ): IOResult[WorkflowNodeId] = { changeStep(from, Cancelled, changeRequestId, actor, reason) } @@ -418,7 +413,7 @@ class TwoValidationStepsWorkflowServiceImpl( changeRequestId: ChangeRequestId, actor: EventActor, reason: Option[String] - ): Box[WorkflowNodeId] = { + ): IOResult[WorkflowNodeId] = { changeStep(Validation, Deployment, changeRequestId, actor, reason) } @@ -426,7 +421,7 @@ class TwoValidationStepsWorkflowServiceImpl( changeRequestId: ChangeRequestId, actor: EventActor, reason: Option[String] - )(implicit qc: QueryContext): Box[WorkflowNodeId] = { + )(implicit qc: QueryContext): IOResult[WorkflowNodeId] = { onSuccessWorkflow(Validation, changeRequestId, actor, reason) } @@ -434,7 +429,7 @@ class TwoValidationStepsWorkflowServiceImpl( changeRequestId: ChangeRequestId, actor: EventActor, reason: Option[String] - ): Box[WorkflowNodeId] = { + ): IOResult[WorkflowNodeId] = { toFailure(Validation, changeRequestId, actor, reason) } @@ -442,7 +437,7 @@ class TwoValidationStepsWorkflowServiceImpl( changeRequestId: ChangeRequestId, actor: EventActor, reason: Option[String] - ): Box[WorkflowNodeId] = { + ): IOResult[WorkflowNodeId] = { toFailure(Deployment, changeRequestId, actor, reason) } @@ -450,7 +445,7 @@ class TwoValidationStepsWorkflowServiceImpl( changeRequestId: ChangeRequestId, actor: EventActor, reason: Option[String] - )(implicit qc: QueryContext): Box[WorkflowNodeId] = { + )(implicit qc: QueryContext): IOResult[WorkflowNodeId] = { onSuccessWorkflow(Deployment, changeRequestId, actor, reason) } diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/api/ChangeRequestApi.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/api/ChangeRequestApi.scala index 37974d390..6d9d81edf 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/api/ChangeRequestApi.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/api/ChangeRequestApi.scala @@ -37,11 +37,18 @@ package com.normation.plugins.changevalidation.api -import com.normation.box.* import com.normation.cfclerk.domain.Technique import com.normation.cfclerk.domain.TechniqueId import com.normation.cfclerk.services.TechniqueRepository -import com.normation.errors.* +import com.normation.errors.AccumulateErrors +import com.normation.errors.BoxToIO +import com.normation.errors.Inconsistency +import com.normation.errors.IOResult +import com.normation.errors.OptionToIoResult +import com.normation.errors.OptionToPureResult +import com.normation.errors.PureResult +import com.normation.errors.PureToIoResult +import com.normation.errors.Unexpected import com.normation.plugins.changevalidation.ChangeRequestFilter import com.normation.plugins.changevalidation.ChangeRequestJson import com.normation.plugins.changevalidation.RoChangeRequestRepository @@ -86,7 +93,6 @@ import com.normation.rudder.services.workflows.CommitAndDeployChangeRequestServi import com.normation.rudder.services.workflows.WorkflowLevelService import com.normation.rudder.users.UserService import enumeratum.* -import net.liftweb.common.Box import net.liftweb.http.LiftResponse import net.liftweb.http.Req import sourcecode.Line @@ -306,7 +312,7 @@ class ChangeRequestApiImpl( ) (_, func) = stepFunc reason <- extractReason(req).toIO - result <- func(changeRequest.id, authzToken.qc.actor, reason).toIO + result <- func(changeRequest.id, authzToken.qc.actor, reason) serialized <- serialize(changeRequest, result).toIO } yield { serialized @@ -343,7 +349,7 @@ class ChangeRequestApiImpl( ) (_, func) = stepFunc reason <- extractReason(req).toIO - result <- func(changeRequest.id, authzToken.qc.actor, reason).toIO + result <- func(changeRequest.id, authzToken.qc.actor, reason) serialized <- serialize(changeRequest, result).toIO } yield { serialized @@ -403,7 +409,7 @@ class ChangeRequestApiImpl( } else { val newCR = ChangeRequest.updateInfo(changeRequest, newInfo) for { - updated <- writeChangeRequest.updateChangeRequest(newCR, authzToken.qc.actor, None).toIO + updated <- writeChangeRequest.updateChangeRequest(newCR, authzToken.qc.actor, None) serialized <- serialize(updated, status).toIO } yield { serialized @@ -425,27 +431,27 @@ class ChangeRequestApiImpl( )( block: (ChangeRequest, WorkflowNodeId, Map[DirectiveId, Technique]) => IOResult[T] ): IOResult[T] = { - val id = { - // PureResult.attempt(s"'${sid}' is not a valid change request id (need to be an integer)")(ChangeRequestId(sid.toInt)) - Box(sid.toIntOption.map(ChangeRequestId(_))) ?~ (s"'${sid}' is not a valid change request id (need to be an integer)") - } - - checkWorkflow match { - case true => - (for { - crId <- id - optCr <- readChangeRequest.get(crId) ?~! (s"Could not find ChangeRequest ${sid}") - changeRequest <- - Box(optCr) ?~ (s"Could not get ChangeRequest ${sid} details cause is: change request with id ${sid} does not exist.") - status <- readWorkflow.getStateOfChangeRequest(crId) ?~! (s"Could not find ChangeRequest ${sid} status") - result <- getDirectiveTechniques(changeRequest).flatMap(block(changeRequest, status, _)).toBox - } yield { - result - }).toIO - .chainError(s"Could not ${actionDetail} ChangeRequest ${sid}") - case false => - disabledWorkflowAnswer + if (checkWorkflow) { + (for { + crId <- sid.toIntOption + .notOptional(s"'${sid}' is not a valid change request id (need to be an integer)") + .map(ChangeRequestId(_)) + changeRequest <- readChangeRequest + .get(crId) + .chainError(s"Could not find ChangeRequest ${sid}") + .notOptional(s"Change request with id ${sid} does not exist.") + status <- readWorkflow + .getStateOfChangeRequest(crId) + .chainError(s"Could not find ChangeRequest ${sid} status") + result <- getDirectiveTechniques(changeRequest) + .flatMap(block(changeRequest, status, _)) + } yield { + result + }) + .chainError(s"Could not ${actionDetail} ChangeRequest ${sid}") + } else { + disabledWorkflowAnswer } } diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/comet/WorkflowInformation.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/comet/WorkflowInformation.scala index a3c7da04d..16abbb1e5 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/comet/WorkflowInformation.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/comet/WorkflowInformation.scala @@ -38,7 +38,6 @@ package com.normation.plugins.changevalidation.comet import bootstrap.liftweb.RudderConfig -import com.normation.box.* import com.normation.plugins.changevalidation.EitherWorkflowService import com.normation.plugins.changevalidation.TwoValidationStepsWorkflowServiceImpl import com.normation.rudder.AuthorizationType @@ -46,7 +45,8 @@ import com.normation.rudder.batch.AsyncWorkflowInfo import com.normation.rudder.services.workflows.WorkflowService import com.normation.rudder.services.workflows.WorkflowUpdate import com.normation.rudder.users.CurrentUser -import net.liftweb.common.* +import com.normation.zio.UnsafeRun +import net.liftweb.common.Loggable import net.liftweb.http.* import scala.xml.* @@ -78,15 +78,16 @@ class WorkflowInformation extends CometActor with CometListener with Loggable { } def render = { - val xml = RudderConfig.configService.rudder_workflow_enabled().toBox match { - case eb: EmptyBox => - val e = eb ?~! "Error when trying to read Rudder configuration for workflow activation" - logger.error(e.messageChain) - e.rootExceptionCause.foreach(ex => logger.error("Exception was:", e)) - (".dropdown-menu *" #> ).apply(layout) - - case Full(workflowEnabled) => + val xml = RudderConfig.configService + .rudder_workflow_enabled() + .chainError("Error when trying to read Rudder configuration for workflow activation") + .either + .runNow match { + case Left(err) => + logger.error(err.fullMsg) + (".dropdown-menu *" #> ).apply(layout) + case Right(workflowEnabled) => val cssSelect = { if (workflowEnabled && (isValidator || isDeployer)) { { @@ -101,7 +102,6 @@ class WorkflowInformation extends CometActor with CometListener with Loggable { ".dropdown *" #> NodeSeq.Empty } } - cssSelect(layout) } @@ -112,10 +112,16 @@ class WorkflowInformation extends CometActor with CometListener with Loggable { workflowService match { case ws: TwoValidationStepsWorkflowServiceImpl => - val validation = - if (isValidator) ws.getItemsInStep(TwoValidationStepsWorkflowServiceImpl.Validation.id).map(_.size).getOrElse(0) else 0 - val deployment = - if (isDeployer) ws.getItemsInStep(TwoValidationStepsWorkflowServiceImpl.Deployment.id).map(_.size).getOrElse(0) else 0 + val validation = { + if (isValidator) + ws.getItemsInStep(TwoValidationStepsWorkflowServiceImpl.Validation.id).map(_.size).orElseSucceed(0).runNow + else 0 + } + val deployment = { + if (isDeployer) + ws.getItemsInStep(TwoValidationStepsWorkflowServiceImpl.Deployment.id).map(_.size).orElseSucceed(0).runNow + else 0 + } validation + deployment case either: EitherWorkflowService => requestCount(either.current) case _ => 0 @@ -130,22 +136,25 @@ class WorkflowInformation extends CometActor with CometListener with Loggable { private[this] def pendingModificationRec(workflowService: WorkflowService): NodeSeq = { workflowService match { - case ws: TwoValidationStepsWorkflowServiceImpl => - ws.getItemsInStep(TwoValidationStepsWorkflowServiceImpl.Validation.id) match { - case Full(seq) => -
  • - - - - Pending review - - {seq.size} - -
  • - case e: EmptyBox => -
  • Error when trying to fetch pending change requests.

  • - } - case either: EitherWorkflowService => pendingModificationRec(either.current) + case ws: TwoValidationStepsWorkflowServiceImpl => + ws.getItemsInStep(TwoValidationStepsWorkflowServiceImpl.Validation.id) + .fold( + _ =>
  • Error when trying to fetch pending change requests.

  • , + seq => { +
  • + + + + Pending review + + {seq.size} + +
  • + } + ) + .runNow + + case either: EitherWorkflowService => pendingModificationRec(either.current) case _ => // For other kind of workflows, this has no meaning
  • Error, the configured workflow does not have that step.

  • } @@ -160,20 +169,22 @@ class WorkflowInformation extends CometActor with CometListener with Loggable { private[this] def pendingDeploymentRec(workflowService: WorkflowService): NodeSeq = { workflowService match { case ws: TwoValidationStepsWorkflowServiceImpl => - ws.getItemsInStep(TwoValidationStepsWorkflowServiceImpl.Deployment.id) match { - case Full(seq) => -
  • - - - - Pending deployment - - {seq.size} - -
  • - case e: EmptyBox => -
  • Error when trying to fetch pending change requests.

  • - } + ws.getItemsInStep(TwoValidationStepsWorkflowServiceImpl.Deployment.id) + .fold( + _ =>
  • Error when trying to fetch pending change requests.

  • , + seq => { +
  • + + + + Pending deployment + + {seq.size} + +
  • + } + ) + .runNow case either: EitherWorkflowService => pendingDeploymentRec(either.current) case _ => // For other kind of workflows, this has no meaning
  • Error, the configured workflow does not have that step.

  • diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestChangesForm.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestChangesForm.scala index 9f4b8e988..8857bd725 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestChangesForm.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestChangesForm.scala @@ -67,6 +67,7 @@ import com.normation.rudder.users.CurrentUser import com.normation.rudder.web.ChooseTemplate import com.normation.rudder.web.model.* import com.normation.utils.DateFormaterService +import com.normation.zio.UnsafeRun import net.liftweb.common.* import net.liftweb.http.* import net.liftweb.http.js.JE.* @@ -108,8 +109,15 @@ class ChangeRequestChangesForm( implicit val qc: QueryContext = CurrentUser.queryContext changeRequest match { case cr: ConfigurationChangeRequest => - ruleCategoryRepository.getRootCategory().toBox match { - case Full(rootRuleCategory) => + ruleCategoryRepository + .getRootCategory() + .chainError("An error occurred when trying to get data from base. ") + .either + .runNow match { + case Left(err) => + logger.error(err.fullMsg) + Text(err.fullMsg) + case Right(rootRuleCategory) => ("#changeTree ul *" #> new ChangesTreeNode(cr, rootRuleCategory).toXml & "#history *" #> displayHistory( rootRuleCategory, @@ -126,11 +134,6 @@ class ChangeRequestChangesForm( cr.globalParams.values.map(_.changes).toList ))(form) ++ Script(JsRaw(s"""buildChangesTree("#changeTree","${S.contextPath}");""")) // JsRaw ok, const - - case eb: EmptyBox => - val e = eb ?~! "An error occurred when trying to get data from base. " - logger.error(e.messageChain) - Text(e.msg) } case _ => Text("not implemented") @@ -267,8 +270,8 @@ class ChangeRequestChangesForm( rules: List[RuleChange] = Nil, globalParams: List[GlobalParameterChange] = Nil )(implicit qc: QueryContext) = { - val crLogs = changeRequestEventLogService.getChangeRequestHistory(changeRequest.id).getOrElse(Seq()) - val wfLogs = workFlowEventLogService.getChangeRequestHistory(changeRequest.id).getOrElse(Seq()) + val crLogs = changeRequestEventLogService.getChangeRequestHistory(changeRequest.id).orElseSucceed(Seq()).runNow + val wfLogs = workFlowEventLogService.getChangeRequestHistory(changeRequest.id).orElseSucceed(Seq()).runNow val lines = { wfLogs.flatMap(displayWorkflowEvent(_)) ++ diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestDetails.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestDetails.scala index 39eb72513..4035f5182 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestDetails.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestDetails.scala @@ -39,8 +39,9 @@ package com.normation.plugins.changevalidation.snippet import bootstrap.liftweb.RudderConfig import bootstrap.rudder.plugin.ChangeValidationConf +import com.normation.box.* +import com.normation.errors.IOResult import com.normation.eventlog.EventActor -import com.normation.eventlog.EventLog import com.normation.plugins.changevalidation.ChangeValidationLogger import com.normation.plugins.changevalidation.TwoValidationStepsWorkflowServiceImpl import com.normation.rudder.AuthorizationType @@ -55,6 +56,7 @@ import com.normation.rudder.users.CurrentUser import com.normation.rudder.web.ChooseTemplate import com.normation.rudder.web.model.* import com.normation.utils.DateFormaterService +import com.normation.zio.UnsafeRun import net.liftweb.common.* import net.liftweb.http.* import net.liftweb.http.js.* @@ -97,7 +99,7 @@ class ChangeRequestDetails extends DispatchSnippet with Loggable { private[this] var changeRequest: Box[ChangeRequest] = { CrId match { case Full(id) => - roChangeRequestRepo.get(ChangeRequestId(id)) match { + roChangeRequestRepo.get(ChangeRequestId(id)).toBox match { case Full(Some(cr)) => if (checkAccess(cr)) Full(cr) @@ -112,9 +114,9 @@ class ChangeRequestDetails extends DispatchSnippet with Loggable { Failure(s"Error in the cr id asked: ${fail.msg}") } } - private[this] def step = changeRequest.flatMap(cr => workflowService.findStep(cr.id)) + private[this] def step = changeRequest.flatMap(cr => workflowService.findStep(cr.id).toBox) -implicit private val qc: QueryContext = CurrentUser.queryContext // bug https://issues.rudder.io/issues/26605 + implicit private val qc: QueryContext = CurrentUser.queryContext // bug https://issues.rudder.io/issues/26605 def dispatch = { // Display Change request Header @@ -223,7 +225,7 @@ implicit private val qc: QueryContext = CurrentUser.queryContext // bug https:// private[this] def changeDetailsCallback(cr: ChangeRequest)(statusUpdate: ChangeRequestInfo)(implicit qc: QueryContext) = { workflowService match { case ws: TwoValidationStepsWorkflowServiceImpl => - val newCR = ws.updateChangeRequestInfo(cr, statusUpdate, CurrentUser.actor, None) + val newCR = ws.updateChangeRequestInfo(cr, statusUpdate, CurrentUser.actor, None).toBox changeRequest = newCR SetHtml("changeRequestHeader", displayHeader(newCR.openOr(cr))) & SetHtml("changeRequestChanges", new ChangeRequestChangesForm(newCR.openOr(cr)).dispatch("changes")(NodeSeq.Empty)) @@ -235,50 +237,65 @@ implicit private val qc: QueryContext = CurrentUser.queryContext // bug https:// } def displayHeader(cr: ChangeRequest)(implicit qc: QueryContext) = { - // last action on the change Request (name/description changed): - val (action, date) = changeRequestEventLogService.getLastLog(cr.id) match { - case eb: EmptyBox => ("Error when retrieving the last action", None) - case Full(None) => ("Error, no action were recorded for that change request", None) // should not happen here ! - case Full(Some(e: EventLog)) => - val actionName = e match { - case _: ModifyChangeRequest => "Modified" - case _: AddChangeRequest => "Created" - case _: DeleteChangeRequest => "Deleted" - } - (s"${actionName} on ${DateFormaterService.getDisplayDate(e.creationDate)} by ${e.principal.name}", Some(e.creationDate)) - } - - // Last workflow change on that change Request - val (step, stepDate) = workFlowEventLogService.getLastLog(cr.id) match { - case eb: EmptyBox => ("Error when retrieving the last action", None) - case Full(None) => ("Error when retrieving the last action", None) // should not happen here ! - case Full(Some(event)) => - val changeStep = eventlogDetailsService - .getWorkflotStepChange(event.details) - .map(step => s"State changed from ${step.from} to ${step.to}") - .getOrElse("Step changed") - - ( - s"${changeStep} on ${DateFormaterService.getDisplayDate(event.creationDate)} by ${event.principal.name}", - Some(event.creationDate) - ) + val (findStep, last) = (for { + // last action on the change Request (name/description changed): + lastCRLog <- changeRequestEventLogService.getLastLog(cr.id).either + (crAct, crDate) = lastCRLog match { + case Left(err) => (s"Error when retrieving the last change request action : ${err.fullMsg}", None) + case Right(None) => (s"Error: no action was recorded for change request with id ${cr.id.value}", None) + case Right(Some(e)) => + val actionName = e match { + case _: ModifyChangeRequest => "Modified" + case _: AddChangeRequest => "Created" + case _: DeleteChangeRequest => "Deleted" + } + ( + s"${actionName} on ${DateFormaterService.getDisplayDate(e.creationDate)} by ${e.principal.name}", + Some(e.creationDate) + ) + } + + // Last workflow change on that change Request + lastWFLog <- workFlowEventLogService.getLastLog(cr.id).either + (wfAct, wfDate) = lastWFLog match { + case Left(err) => (s"Error when retrieving the last workflow change : ${err.fullMsg}", None) + case Right(None) => (s"Error: no action was recorded for change request with id ${cr.id.value}", None) + case Right(Some(e)) => + val changeStep = eventlogDetailsService + .getWorkflotStepChange(e.details) + .map(step => s"State changed from ${step.from} to ${step.to}") + .getOrElse("Step changed") + + ( + s"${changeStep} on ${DateFormaterService.getDisplayDate(e.creationDate)} by ${e.principal.name}", + Some(e.creationDate) + ) + } + + last = (crDate, wfDate) match { + case (Some(crd), Some(wfd)) => if (crd.isAfter(wfd)) crAct else wfAct + case (None, Some(_)) => wfAct + case (_, None) => crAct + } + findStep <- workflowService + .findStep(cr.id) + .either + } yield { + (findStep, last) + }).runNow + + val (crStatus, actionBtns) = findStep match { + case Left(err) => + (
    Cannot find the status of this change request
    , NodeSeq.Empty) + case Right(step) => + (Text(step.value), displayActionButton(cr, step)) } - // Compare both to find the oldest - val last = (date, stepDate) match { - case (None, None) => action - case (Some(_), None) => action - case (None, Some(_)) => step - case (Some(date), Some(stepDate)) => if (date.isAfter(stepDate)) action else step - } ("#backButton [href]" #> "/secure/configurationManager/changes/changeRequests" & "#nameTitle *" #> s"CR #${cr.id}: ${cr.info.name}" & - "#CRStatus *" #> workflowService - .findStep(cr.id) - .map(x => Text(x.value)) - .openOr(
    Cannot find the status of this change request
    ) & + "#CRStatus *" #> crStatus & "#CRLastAction *" #> s"${last}" & - "#actionBtns *" #> workflowService.findStep(cr.id).map(x => displayActionButton(cr, x)).openOr(NodeSeq.Empty))(header) + "#actionBtns *" #> actionBtns)(header) } @@ -294,10 +311,10 @@ implicit private val qc: QueryContext = CurrentUser.queryContext // bug https:// def ChangeStepPopup( action: String, - nextSteps: Seq[(WorkflowNodeId, (ChangeRequestId, EventActor, Option[String]) => Box[WorkflowNodeId])], + nextSteps: Seq[(WorkflowNodeId, (ChangeRequestId, EventActor, Option[String]) => IOResult[WorkflowNodeId])], cr: ChangeRequest )(implicit qc: QueryContext) = { - type stepChangeFunction = (ChangeRequestId, EventActor, Option[String]) => Box[WorkflowNodeId] + type stepChangeFunction = (ChangeRequestId, EventActor, Option[String]) => IOResult[WorkflowNodeId] def closePopup: JsCmd = { SetHtml("changeRequestHeader", displayHeader(cr)) & @@ -306,7 +323,8 @@ implicit private val qc: QueryContext = CurrentUser.queryContext // bug https:// workflowService .findStep(cr.id) .map(x => Text(x.value)) - .openOr(
    Cannot find the status of this change request
    ) + .orElseSucceed(
    Cannot find the status of this change request
    ) + .runNow ) & SetHtml("changeRequestChanges", new ChangeRequestChangesForm(cr).dispatch("changes")(NodeSeq.Empty)) & JsRaw("""hideBsModal('popupContent');""") // JsRaw ok, const @@ -408,22 +426,26 @@ implicit private val qc: QueryContext = CurrentUser.queryContext // bug https:// def error(msg: String) =
    {msg}
    def confirm(): JsCmd = { + val user = CurrentUser.actor + if (formTracker.hasErrors) { formTracker.addFormError(error("There was problem with your request")) updateForm(nextChosen) } else { - nextChosen._2(cr.id, CurrentUser.actor, changeMessage.map(_.get)) match { - case Full(next) => + val (_, evalNextStep) = nextChosen + evalNextStep(cr.id, user, changeMessage.map(_.get)) + .chainError("could not change Change request step") + .either + .runNow match { + case Left(err) => + formTracker.addFormError(error(err.fullMsg)) + logger.error(s"Error when saving change request '${cr.id.value}': ${err.fullMsg}") + updateForm(nextChosen) + case Right(next) => SetHtml("workflowActionButtons", displayActionButton(cr, next)) & SetHtml("newStatus", Text(next.value)) & closePopup & JsRaw(""" initBsModal("successWorkflow"); """) // JsRaw ok, const - case eb: EmptyBox => - val fail = eb ?~! "could not change Change request step" - formTracker.addFormError(error(fail.msg)) - logger.error(s"Error when saving change request '${cr.id.value}': ${fail.messageChain}") - updateForm(nextChosen) } - } } diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestManagement.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestManagement.scala index f2f96c06b..010017e5b 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestManagement.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestManagement.scala @@ -48,6 +48,7 @@ import com.normation.rudder.users.CurrentUser import com.normation.rudder.web.services.JsTableData import com.normation.rudder.web.services.JsTableLine import com.normation.utils.DateFormaterService +import com.normation.zio.UnsafeRun import net.liftweb.common.* import net.liftweb.http.* import net.liftweb.http.DispatchSnippet @@ -61,6 +62,8 @@ import org.apache.commons.text.StringEscapeUtils import scala.xml.Elem import scala.xml.NodeSeq import scala.xml.Text +import zio.UIO +import zio.syntax.* class ChangeRequestManagement extends DispatchSnippet with Loggable { @@ -113,29 +116,32 @@ class ChangeRequestManagement extends DispatchSnippet with Loggable { } } - def getLines() = { + def getLines(): UIO[JsTableData[ChangeRequestLine]] = { val changeRequests = if (currentUser) roCrRepo.getAll() else roCrRepo.getByContributor(CurrentUser.actor) - JsTableData(changeRequests match { - case Full(changeRequests) => - val eventMap = getLastEventsMap - val workflowStateMap: Map[ChangeRequestId, WorkflowNodeId] = workflowService.getAllChangeRequestsStep() match { - case Full(stateMap) => stateMap - case eb: EmptyBox => - val fail = eb ?~! "Could not find change requests state" - logger.error(fail.messageChain) - Map() - } - changeRequests.map(ChangeRequestLine(_, workflowStateMap, eventMap)).toList - case eb: EmptyBox => - val fail = eb ?~! s"Could not get change requests because of : ${eb}" - logger.error(fail.msg) - Nil - }) + for { + crs <- changeRequests + .chainError("Could not get change requests") + .catchAll(err => { + logger.error(err.fullMsg) + Vector().succeed + }) + workflowStateMap <- workflowService + .getAllChangeRequestsStep() + .chainError("Could not find change requests state") + .catchAll(err => { + logger.error(err.fullMsg) + Map.empty[ChangeRequestId, WorkflowNodeId].succeed + }) + lastEventsMap <- getLastEventsMap + } yield { + JsTableData(crs.map(ChangeRequestLine(_, workflowStateMap, lastEventsMap)).toList) + } } + def dataTableInit = { val refresh = AnonFunc( - SHtml.ajaxInvoke(() => JsRaw(s"refreshTable('${changeRequestTableId}',${getLines().toJson.toJsCmd})")) + SHtml.ajaxInvoke(() => JsRaw(s"refreshTable('${changeRequestTableId}',${getLines().runNow.toJson.toJsCmd})")) ) // JsRaw ok, from json val filter = initFilter match { @@ -155,36 +161,38 @@ class ChangeRequestManagement extends DispatchSnippet with Loggable { /** * Get all events, merge them via a mutMap (we only want to keep the most recent event) */ - private[this] def getLastEventsMap = { - val CREventsMap: Map[ChangeRequestId, EventLog] = changeRequestEventLogService.getLastCREvents match { - case Full(map) => map - case eb: EmptyBox => - val fail = eb ?~! "Could not find last Change requests events requests state" - logger.error(fail.messageChain) - Map() - } - val workflowEventsMap: Map[ChangeRequestId, EventLog] = workflowLoggerService.getLastWorkflowEvents() match { - case Full(map) => map - case eb: EmptyBox => - val fail = eb ?~! "Could not find last Change requests events requests state" - logger.error(fail.messageChain) - Map() - } + private[this] def getLastEventsMap: UIO[Map[ChangeRequestId, EventLog]] = { - import scala.collection.mutable.Map as MutMap + for { + crEventsMap <- changeRequestEventLogService.getLastCREvents + .chainError("Could not find last Change requests events requests state") + .catchAll(err => { + logger.error(err.fullMsg) + Map.empty[ChangeRequestId, EventLog].succeed + }) + workflowEventsMap <- workflowLoggerService + .getLastWorkflowEvents() + .chainError("Could not find last Change requests events requests state") + .catchAll(err => { + logger.error(err.fullMsg) + Map.empty[ChangeRequestId, EventLog].succeed + }) + } yield { + import scala.collection.mutable.Map as MutMap - val eventMap = MutMap() ++ CREventsMap + val eventMap = MutMap() ++ crEventsMap - for { - (crId, event) <- workflowEventsMap - } { - eventMap.get(crId) match { - case Some(currentEvent) if (currentEvent.creationDate isAfter event.creationDate) => - case _ => eventMap.update(crId, event) + for { + (crId, event) <- workflowEventsMap + } { + eventMap.get(crId) match { + case Some(currentEvent) if (currentEvent.creationDate isAfter event.creationDate) => + case _ => eventMap.update(crId, event) + } } - } - eventMap.toMap + eventMap.toMap + } } def statusFilter = { diff --git a/change-validation/src/test/resources/changevalidation_api/api_changerequest.yml b/change-validation/src/test/resources/changevalidation_api/api_changerequest.yml index d8046c921..bf664b1b3 100644 --- a/change-validation/src/test/resources/changevalidation_api/api_changerequest.yml +++ b/change-validation/src/test/resources/changevalidation_api/api_changerequest.yml @@ -3050,7 +3050,7 @@ response: "action": "changeRequestDetails", "id": "999", "result": "error", - "errorDetails": "Could not find ChangeRequest 999; cause was: Unexpected: Could not get ChangeRequest 999 details cause is: change request with id 999 does not exist." + "errorDetails": "Could not find ChangeRequest 999; cause was: Inconsistency: Change request with id 999 does not exist." } --- description: Get information about given change request when id is not a number @@ -3063,7 +3063,7 @@ response: "action": "changeRequestDetails", "id": "aaa", "result": "error", - "errorDetails": "Could not find ChangeRequest aaa; cause was: Unexpected: 'aaa' is not a valid change request id (need to be an integer)" + "errorDetails": "Could not find ChangeRequest aaa; cause was: Inconsistency: 'aaa' is not a valid change request id (need to be an integer)" } --- description: Decline given change request @@ -3301,7 +3301,7 @@ response: "action": "declineChangeRequest", "id": "12", "result": "error", - "errorDetails": "Could not decline ChangeRequest 12; cause was: Unexpected: Inconsistency: Could not decline ChangeRequest 12 details cause is: could not decline ChangeRequest 12, because status 'Cancelled' cannot be cancelled." + "errorDetails": "Could not decline ChangeRequest 12; cause was: Inconsistency: Could not decline ChangeRequest 12 details cause is: could not decline ChangeRequest 12, because status 'Cancelled' cannot be cancelled." } --- description: Decline given change request when id is unknown @@ -3314,7 +3314,7 @@ response: "action": "declineChangeRequest", "id": "999", "result": "error", - "errorDetails": "Could not decline ChangeRequest 999; cause was: Unexpected: Could not get ChangeRequest 999 details cause is: change request with id 999 does not exist." + "errorDetails": "Could not decline ChangeRequest 999; cause was: Inconsistency: Change request with id 999 does not exist." } --- description: Decline given change request when id is not a number @@ -3327,7 +3327,7 @@ response: "action": "declineChangeRequest", "id": "aaa", "result": "error", - "errorDetails": "Could not decline ChangeRequest aaa; cause was: Unexpected: 'aaa' is not a valid change request id (need to be an integer)" + "errorDetails": "Could not decline ChangeRequest aaa; cause was: Inconsistency: 'aaa' is not a valid change request id (need to be an integer)" } --- description: Accept given change request @@ -3586,7 +3586,7 @@ response: "action": "acceptChangeRequest", "id": "999", "result": "error", - "errorDetails": "Could not accept ChangeRequest 999; cause was: Unexpected: Could not get ChangeRequest 999 details cause is: change request with id 999 does not exist." + "errorDetails": "Could not accept ChangeRequest 999; cause was: Inconsistency: Change request with id 999 does not exist." } --- description: Accept given change request when id is not a number @@ -3603,7 +3603,7 @@ response: "action": "acceptChangeRequest", "id": "aaa", "result": "error", - "errorDetails": "Could not accept ChangeRequest aaa; cause was: Unexpected: 'aaa' is not a valid change request id (need to be an integer)" + "errorDetails": "Could not accept ChangeRequest aaa; cause was: Inconsistency: 'aaa' is not a valid change request id (need to be an integer)" } --- description: Update information about given change request @@ -3704,7 +3704,7 @@ response: "action": "updateChangeRequest", "id": "999", "result": "error", - "errorDetails": "Could not update ChangeRequest 999; cause was: Unexpected: Could not get ChangeRequest 999 details cause is: change request with id 999 does not exist." + "errorDetails": "Could not update ChangeRequest 999; cause was: Inconsistency: Change request with id 999 does not exist." } --- description: Update information about given change request when id is not a number @@ -3722,7 +3722,7 @@ response: "action": "updateChangeRequest", "id": "aaa", "result": "error", - "errorDetails": "Could not update ChangeRequest aaa; cause was: Unexpected: 'aaa' is not a valid change request id (need to be an integer)" + "errorDetails": "Could not update ChangeRequest aaa; cause was: Inconsistency: 'aaa' is not a valid change request id (need to be an integer)" } --- description: Update information about given change request when no parameter is sent @@ -3735,7 +3735,7 @@ response: "action": "updateChangeRequest", "id": "4", "result": "error", - "errorDetails": "Could not update ChangeRequest 4; cause was: Unexpected: Inconsistency: Could not update ChangeRequest 4 details cause is: No changes to save." + "errorDetails": "Could not update ChangeRequest 4; cause was: Inconsistency: Could not update ChangeRequest 4 details cause is: No changes to save." } --- description: Update information about given change request without changing any info @@ -3753,5 +3753,5 @@ response: "action" : "updateChangeRequest", "id" : "11", "result" : "error", - "errorDetails" : "Could not update ChangeRequest 11; cause was: Unexpected: Inconsistency: Could not update ChangeRequest 11 details cause is: No changes to save." + "errorDetails" : "Could not update ChangeRequest 11; cause was: Inconsistency: Could not update ChangeRequest 11 details cause is: No changes to save." } diff --git a/change-validation/src/test/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepositoryTest.scala b/change-validation/src/test/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepositoryTest.scala index fb46bbad0..dfce81474 100644 --- a/change-validation/src/test/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepositoryTest.scala +++ b/change-validation/src/test/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepositoryTest.scala @@ -44,6 +44,7 @@ import com.normation.BoxSpecMatcher import com.normation.GitVersion import com.normation.cfclerk.domain.TechniqueName import com.normation.cfclerk.domain.TechniqueVersionHelper +import com.normation.errors.Inconsistency import com.normation.eventlog.EventActor import com.normation.eventlog.ModificationId import com.normation.rudder.db.DBCommon @@ -66,8 +67,6 @@ import com.typesafe.config.ConfigValueFactory import doobie.Transactor import doobie.specs2.analysisspec.IOChecker import doobie.syntax.all.* -import net.liftweb.common.Box -import net.liftweb.common.Failure import net.liftweb.common.Full import org.joda.time.DateTime import org.junit.runner.RunWith @@ -288,13 +287,14 @@ class ChangeRequestJdbcRepositoryTest extends Specification with DBCommon with I } "get all change requests" in { - val res = roChangeRequestJdbcRepository.getAll() - (res.map(_.size) must beEqualTo(Full(1))) and (res.flatMap(_.headOption) mustFullEq expectedChangeRequest) + val res = roChangeRequestJdbcRepository.getAll().runNow + res.size must beEqualTo(1) + res.headOption must beSome(expectedChangeRequest) } "get a change request by id" in { - val res = roChangeRequestJdbcRepository.get(changeRequestId) - res.flatMap(Box(_)) mustFullEq expectedChangeRequest + val res = roChangeRequestJdbcRepository.get(changeRequestId).runNow + res must beSome(expectedChangeRequest) } // This does not seem to return any result at all, but the xpath and query look okay... :( @@ -304,16 +304,18 @@ class ChangeRequestJdbcRepositoryTest extends Specification with DBCommon with I // } "get change requests by xpath content" in { - val resDirective = roChangeRequestJdbcRepository.getByDirective(DirectiveUid("foo"), false) - val resNodeGroup = roChangeRequestJdbcRepository.getByNodeGroup(NodeGroupId(NodeGroupUid("bar")), false) - val resRule = roChangeRequestJdbcRepository.getByRule(RuleUid("baz"), false) - ((resDirective - .map(_.size) must beEqualTo(Full(1))) and (resDirective.flatMap(_.headOption) mustFullEq expectedChangeRequest) and - (resNodeGroup.map(_.size) must beEqualTo(Full(1))) and (resNodeGroup.flatMap( - _.headOption - ) mustFullEq expectedChangeRequest) and - (resRule.map(_.size) must beEqualTo(Full(1))) and (resRule.flatMap(_.headOption) mustFullEq expectedChangeRequest)) + val resDirective = roChangeRequestJdbcRepository.getByDirective(DirectiveUid("foo"), false).runNow + val resNodeGroup = roChangeRequestJdbcRepository.getByNodeGroup(NodeGroupId(NodeGroupUid("bar")), false).runNow + val resRule = roChangeRequestJdbcRepository.getByRule(RuleUid("baz"), false).runNow + + resDirective.size must beEqualTo(1) + resDirective.headOption must beSome(expectedChangeRequest) + resNodeGroup.size must beEqualTo(1) + resNodeGroup.headOption must beSome(expectedChangeRequest) + + resRule.size must beEqualTo(1) + resRule.headOption must beSome(expectedChangeRequest) } "get change request by filter" in { @@ -328,7 +330,7 @@ class ChangeRequestJdbcRepositoryTest extends Specification with DBCommon with I actor, Some("reason") ) - res mustFullEq newChangeRequest + res.runNow must beEqualTo(newChangeRequest) } "delete change request (unimplemented)" in { @@ -345,15 +347,18 @@ class ChangeRequestJdbcRepositoryTest extends Specification with DBCommon with I actor, Some("reason") ) - res mustFullEq updatedChangeRequest + res.runNow must beEqualTo(updatedChangeRequest) } "update a non-existing change request" in { - woChangeRequestJdbcRepository.updateChangeRequest( - expectedChangeRequest.copy(id = ChangeRequestId(999)), - actor, - Some("reason") - ) must beEqualTo(Failure(s"Cannot update non-existent Change Request with id 999")) + woChangeRequestJdbcRepository + .updateChangeRequest( + expectedChangeRequest.copy(id = ChangeRequestId(999)), + actor, + Some("reason") + ) + .either + .runNow must beLeft(Inconsistency(s"Cannot update non-existent Change Request with id 999")) } } diff --git a/change-validation/src/test/scala/com/normation/plugins/changevalidation/MockServices.scala b/change-validation/src/test/scala/com/normation/plugins/changevalidation/MockServices.scala index aef9e2046..0a9e2f67e 100644 --- a/change-validation/src/test/scala/com/normation/plugins/changevalidation/MockServices.scala +++ b/change-validation/src/test/scala/com/normation/plugins/changevalidation/MockServices.scala @@ -74,8 +74,6 @@ import com.normation.rudder.users.AuthenticatedUser import com.normation.rudder.users.RudderAccount import com.normation.rudder.users.UserService import com.normation.zio.UnsafeRun -import net.liftweb.common.Box -import net.liftweb.common.Full import scala.collection.immutable.SortedMap import zio.Chunk import zio.Ref @@ -178,10 +176,10 @@ class MockServices(changeRequestsByStatus: Map[WorkflowNodeId, List[ChangeReques .succeed } - override def get(changeRequestId: ChangeRequestId): Box[Option[ChangeRequest]] = { + override def get(changeRequestId: ChangeRequestId): IOResult[Option[ChangeRequest]] = { changeRequestsByStatus.values.flatten.find(_.id == changeRequestId) match { - case Some(cr) => Full(Some(cr)) - case None => Full(None) + case Some(cr) => Some(cr).succeed + case None => None.succeed } } @@ -189,61 +187,59 @@ class MockServices(changeRequestsByStatus: Map[WorkflowNodeId, List[ChangeReques changeRequest: ChangeRequest, actor: EventActor, reason: Option[String] - ): Box[ChangeRequest] = { - Full(changeRequest) + ): IOResult[ChangeRequest] = { + changeRequest.succeed } - override def getAll(): Box[Vector[ChangeRequest]] = ??? + override def getAll(): IOResult[Vector[ChangeRequest]] = ??? - override def getByDirective(id: DirectiveUid, onlyPending: Boolean): Box[Vector[ChangeRequest]] = ??? + override def getByDirective(id: DirectiveUid, onlyPending: Boolean): IOResult[Vector[ChangeRequest]] = ??? - override def getByNodeGroup(id: NodeGroupId, onlyPending: Boolean): Box[Vector[ChangeRequest]] = ??? + override def getByNodeGroup(id: NodeGroupId, onlyPending: Boolean): IOResult[Vector[ChangeRequest]] = ??? - override def getByRule(id: RuleUid, onlyPending: Boolean): Box[Vector[ChangeRequest]] = ??? + override def getByRule(id: RuleUid, onlyPending: Boolean): IOResult[Vector[ChangeRequest]] = ??? - override def getByContributor(actor: EventActor): Box[Vector[ChangeRequest]] = ??? + override def getByContributor(actor: EventActor): IOResult[Vector[ChangeRequest]] = ??? override def createChangeRequest( changeRequest: ChangeRequest, actor: EventActor, reason: Option[String] - ): Box[ChangeRequest] = ??? + ): IOResult[ChangeRequest] = ??? override def deleteChangeRequest( changeRequestId: ChangeRequestId, actor: EventActor, reason: Option[String] - ): Box[ChangeRequest] = ??? + ): IOResult[ChangeRequest] = ??? } object workflowRepository extends RoWorkflowRepository with WoWorkflowRepository { - override def getStateOfChangeRequest(crId: ChangeRequestId): Box[WorkflowNodeId] = { + override def getStateOfChangeRequest(crId: ChangeRequestId): IOResult[WorkflowNodeId] = { changeRequestsByStatus.find(_._2.exists(_.id == crId)).map(_._1) match { - case Some(state) => Full(state) - case None => Full(WorkflowNodeId("unknown")) + case Some(state) => state.succeed + case None => WorkflowNodeId("unknown").succeed } } - override def updateState(crId: ChangeRequestId, from: WorkflowNodeId, state: WorkflowNodeId): Box[WorkflowNodeId] = { - Full(state) + override def updateState(crId: ChangeRequestId, from: WorkflowNodeId, state: WorkflowNodeId): IOResult[WorkflowNodeId] = { + state.succeed } - override def getAllByState(state: WorkflowNodeId): Box[Seq[ChangeRequestId]] = { + override def getAllByState(state: WorkflowNodeId): IOResult[Seq[ChangeRequestId]] = { ??? } - override def createWorkflow(crId: ChangeRequestId, state: WorkflowNodeId): Box[WorkflowNodeId] = ??? + override def createWorkflow(crId: ChangeRequestId, state: WorkflowNodeId): IOResult[WorkflowNodeId] = ??? - override def getAllChangeRequestsState(): Box[Map[ChangeRequestId, WorkflowNodeId]] = ??? + override def getAllChangeRequestsState(): IOResult[Map[ChangeRequestId, WorkflowNodeId]] = ??? } object commitAndDeployChangeRequest extends CommitAndDeployChangeRequestService { - def save(changeRequest: ChangeRequest)(implicit cc: ChangeContext): Box[ChangeRequest] = Full( - changeRequest - ) + def save(changeRequest: ChangeRequest)(implicit cc: ChangeContext): IOResult[ChangeRequest] = changeRequest.succeed def isMergeable(changeRequest: ChangeRequest)(implicit qc: QueryContext): Boolean = { // can depend on "mergeable" changeRequest by their id to vary test cases @@ -253,13 +249,14 @@ class MockServices(changeRequestsByStatus: Map[WorkflowNodeId, List[ChangeReques } object workflowEventLogService extends WorkflowEventLogService { - override def saveEventLog(stepChange: WorkflowStepChange, actor: EventActor, reason: Option[String]): Box[EventLog] = Full( - null - ) + override def saveEventLog(stepChange: WorkflowStepChange, actor: EventActor, reason: Option[String]): IOResult[EventLog] = { + val res: EventLog = null + res.succeed + } - override def getChangeRequestHistory(id: ChangeRequestId): Box[Seq[WorkflowStepChanged]] = ??? - override def getLastLog(id: ChangeRequestId): Box[Option[WorkflowStepChanged]] = ??? - override def getLastWorkflowEvents(): Box[Map[ChangeRequestId, EventLog]] = ??? + override def getChangeRequestHistory(id: ChangeRequestId): IOResult[Seq[WorkflowStepChanged]] = ??? + override def getLastLog(id: ChangeRequestId): IOResult[Option[WorkflowStepChanged]] = ??? + override def getLastWorkflowEvents(): IOResult[Map[ChangeRequestId, EventLog]] = ??? } @@ -270,12 +267,15 @@ class MockServices(changeRequestsByStatus: Map[WorkflowNodeId, List[ChangeReques principal: EventActor, diff: ChangeRequestDiff, reason: Option[String] - ): Box[EventLog] = Full(null) + ): IOResult[EventLog] = { + val res: EventLog = null + res.succeed + } - override def getChangeRequestHistory(id: ChangeRequestId): Box[Seq[ChangeRequestEventLog]] = ??? - override def getFirstLog(id: ChangeRequestId): Box[Option[ChangeRequestEventLog]] = ??? - override def getLastLog(id: ChangeRequestId): Box[Option[ChangeRequestEventLog]] = ??? - override def getLastCREvents: Box[Map[ChangeRequestId, EventLog]] = ??? + override def getChangeRequestHistory(id: ChangeRequestId): IOResult[Seq[ChangeRequestEventLog]] = ??? + override def getFirstLog(id: ChangeRequestId): IOResult[Option[ChangeRequestEventLog]] = ??? + override def getLastLog(id: ChangeRequestId): IOResult[Option[ChangeRequestEventLog]] = ??? + override def getLastCREvents: IOResult[Map[ChangeRequestId, EventLog]] = ??? } object notificationService extends NotificationService { diff --git a/change-validation/src/test/scala/com/normation/plugins/changevalidation/ValidationNeededTest.scala b/change-validation/src/test/scala/com/normation/plugins/changevalidation/ValidationNeededTest.scala index 0b1eb15b0..d79335593 100644 --- a/change-validation/src/test/scala/com/normation/plugins/changevalidation/ValidationNeededTest.scala +++ b/change-validation/src/test/scala/com/normation/plugins/changevalidation/ValidationNeededTest.scala @@ -57,7 +57,7 @@ import com.normation.rudder.services.workflows.GlobalParamModAction import com.normation.rudder.services.workflows.NodeGroupChangeRequest import com.normation.rudder.services.workflows.RuleChangeRequest import com.normation.rudder.services.workflows.RuleModAction -import net.liftweb.common.Full +import com.normation.zio.UnsafeRun import org.junit.runner.RunWith import org.specs2.mutable.Specification import org.specs2.runner.JUnitRunner @@ -83,19 +83,19 @@ class ValidationNeededTest extends Specification { "always validate a modification in a GlobalParam" in { val globalParamChangeReq = GlobalParamChangeRequest(GlobalParamModAction.Create, None) - nodeGrpValNdd.forGlobalParam(actor, globalParamChangeReq) must beEqualTo(Full(true)) + nodeGrpValNdd.forGlobalParam(actor, globalParamChangeReq).runNow must beTrue } "validate a modification in a node group if that group is supervised" in { val nodeGrp = mockNodeGroups.g0 val nodeGrpChangeReq = NodeGroupChangeRequest(DGModAction.CreateSolo, nodeGrp, None, None) - nodeGrpValNdd.forNodeGroup(actor, nodeGrpChangeReq) must beEqualTo(Full(true)) + nodeGrpValNdd.forNodeGroup(actor, nodeGrpChangeReq).runNow must beTrue } "not validate a modification in a node group if that group is not supervised" in { val nodeGrp = mockNodeGroups.g1 val nodeGrpChangeReq = NodeGroupChangeRequest(DGModAction.CreateSolo, nodeGrp, None, None) - nodeGrpValNdd.forNodeGroup(actor, nodeGrpChangeReq) must beEqualTo(Full(false)) + nodeGrpValNdd.forNodeGroup(actor, nodeGrpChangeReq).runNow must beFalse } "if a modification concerns a rule that" in { @@ -109,7 +109,7 @@ class ValidationNeededTest extends Specification { rule, None ) - nodeGrpValNdd.forRule(actor, ruleChangeReq) must beEqualTo(Full(true)) + nodeGrpValNdd.forRule(actor, ruleChangeReq).runNow must beTrue } "doesn't target a supervised node, the modification mustn't be validated" in { @@ -121,7 +121,7 @@ class ValidationNeededTest extends Specification { rule, None ) - nodeGrpValNdd.forRule(actor, ruleChangeReq) must beEqualTo(Full(false)) + nodeGrpValNdd.forRule(actor, ruleChangeReq).runNow must beFalse } } @@ -144,7 +144,7 @@ class ValidationNeededTest extends Specification { List(mockRules.rules.defaultRule) // targets all nodes, including node group g0 ) - nodeGrpValNdd.forDirective(actor, dirChangeReq) must beEqualTo(Full(true)) + nodeGrpValNdd.forDirective(actor, dirChangeReq).runNow must beTrue } "not validate a modification in a directive if no rule that uses it requires validation" in { @@ -161,7 +161,7 @@ class ValidationNeededTest extends Specification { ) - nodeGrpValNdd.forDirective(actor, dirChangeReq) must beEqualTo(Full(false)) + nodeGrpValNdd.forDirective(actor, dirChangeReq).runNow must beFalse } } diff --git a/change-validation/src/test/scala/com/normation/plugins/changevalidation/WorkflowJdbcRepositoryTest.scala b/change-validation/src/test/scala/com/normation/plugins/changevalidation/WorkflowJdbcRepositoryTest.scala index ce9e32612..3c7c438aa 100644 --- a/change-validation/src/test/scala/com/normation/plugins/changevalidation/WorkflowJdbcRepositoryTest.scala +++ b/change-validation/src/test/scala/com/normation/plugins/changevalidation/WorkflowJdbcRepositoryTest.scala @@ -41,10 +41,10 @@ import cats.effect.IO import com.normation.rudder.db.DBCommon import com.normation.rudder.domain.workflows.ChangeRequestId import com.normation.rudder.domain.workflows.WorkflowNodeId +import com.normation.zio.UnsafeRun import doobie.Transactor import doobie.specs2.analysisspec.IOChecker import doobie.syntax.all.* -import net.liftweb.common.Full import org.junit.runner.RunWith import org.specs2.mutable.Specification import org.specs2.runner.JUnitRunner @@ -87,27 +87,27 @@ class WorkflowJdbcRepositoryTest extends Specification with DBCommon with IOChec val firstWorkflowNodeId = WorkflowNodeId("first") val secondWorkflowNodeId = WorkflowNodeId("second") "create a workflow" in { - woWorkflowJdbcRepository.createWorkflow(changeRequestId, firstWorkflowNodeId) must beEqualTo(Full(firstWorkflowNodeId)) + woWorkflowJdbcRepository.createWorkflow(changeRequestId, firstWorkflowNodeId).runNow must beEqualTo(firstWorkflowNodeId) } "update a workflow" in { - woWorkflowJdbcRepository.updateState(changeRequestId, firstWorkflowNodeId, secondWorkflowNodeId) must beEqualTo( - Full(secondWorkflowNodeId) + woWorkflowJdbcRepository.updateState(changeRequestId, firstWorkflowNodeId, secondWorkflowNodeId).runNow must beEqualTo( + secondWorkflowNodeId ) } "get all change requests by state" in { - roWorkflowJdbcRepository.getAllByState(firstWorkflowNodeId) must beEqualTo(Full(Seq.empty)) - roWorkflowJdbcRepository.getAllByState(secondWorkflowNodeId) must beEqualTo(Full(Vector(changeRequestId))) + roWorkflowJdbcRepository.getAllByState(firstWorkflowNodeId).runNow must beEqualTo(Seq.empty) + roWorkflowJdbcRepository.getAllByState(secondWorkflowNodeId).runNow must beEqualTo(Vector(changeRequestId)) } "get state of change request" in { - roWorkflowJdbcRepository.getStateOfChangeRequest(changeRequestId) must beEqualTo(Full(secondWorkflowNodeId)) + roWorkflowJdbcRepository.getStateOfChangeRequest(changeRequestId).runNow must beEqualTo(secondWorkflowNodeId) } "get all change requests state" in { - roWorkflowJdbcRepository.getAllChangeRequestsState() must beEqualTo(Full(Map(changeRequestId -> secondWorkflowNodeId))) + roWorkflowJdbcRepository.getAllChangeRequestsState().runNow must beEqualTo(Map(changeRequestId -> secondWorkflowNodeId)) } } diff --git a/change-validation/src/test/scala/com/normation/plugins/changevalidation/api/ChangeRequestApiTest.scala b/change-validation/src/test/scala/com/normation/plugins/changevalidation/api/ChangeRequestApiTest.scala index c0fc625ea..73058ae50 100644 --- a/change-validation/src/test/scala/com/normation/plugins/changevalidation/api/ChangeRequestApiTest.scala +++ b/change-validation/src/test/scala/com/normation/plugins/changevalidation/api/ChangeRequestApiTest.scala @@ -96,10 +96,10 @@ import com.normation.rudder.rest.RestTestSetUp import com.normation.rudder.rest.TraitTestApiFromYamlFiles import com.normation.rudder.services.modification.DiffServiceImpl import java.nio.file.Files -import net.liftweb.common.Full import org.joda.time.DateTime import org.junit.runner.RunWith import zio.* +import zio.syntax.* import zio.test.* import zio.test.junit.ZTestJUnitRunner @@ -624,9 +624,9 @@ class ChangeRequestApiTest extends ZIOSpecDefault { mockServices.changeRequestRepository, mockServices.notificationService, mockServices.userService, - () => Full(true), - () => Full(true), - () => Full(true) + () => true.succeed, + () => true.succeed, + () => true.succeed ) restTestSetUp.workflowLevelService.overrideLevel( @@ -635,8 +635,8 @@ class ChangeRequestApiTest extends ZIOSpecDefault { restTestSetUp.workflowLevelService.defaultWorkflowService, validationWorkflowService, List.empty, - () => Full(true), - () => Full(false), + () => true.succeed, + () => false.succeed, null ) ) From 42fe2a30f230a2d3ea273ad22dc16c3dc3e46543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Membr=C3=A9?= Date: Tue, 22 Apr 2025 15:11:07 +0200 Subject: [PATCH 42/65] Prepare next nightly branch of plugin --- main-build.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main-build.conf b/main-build.conf index fbf9bdb5c..b0eedbc52 100644 --- a/main-build.conf +++ b/main-build.conf @@ -14,6 +14,6 @@ # Version of Rudder used to build the plugin. # It defined the API/ABI used and it is important for binary compatibility branch-type=next -rudder-version=8.3.0~rc2 +rudder-version=8.3.0 common-version=2.1.1 private-version=2.1.0 From 67c43ae847b795b8a12e6c7075ca781ca0db1ffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Membr=C3=A9?= Date: Tue, 22 Apr 2025 15:14:12 +0200 Subject: [PATCH 43/65] Prepare next nightly branch of plugin --- main-build.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main-build.conf b/main-build.conf index b0eedbc52..e91e03123 100644 --- a/main-build.conf +++ b/main-build.conf @@ -14,6 +14,6 @@ # Version of Rudder used to build the plugin. # It defined the API/ABI used and it is important for binary compatibility branch-type=next -rudder-version=8.3.0 +rudder-version=8.3.1 common-version=2.1.1 private-version=2.1.0 From c39829758ee99f3ed3762144eb1a85574fbd315f Mon Sep 17 00:00:00 2001 From: vhayaert Date: Thu, 17 Apr 2025 13:00:17 +0200 Subject: [PATCH 44/65] Fixes #26760: Add a new internal endpoint in order to replace WorkflowInformation --- .../rudder/plugin/ChangeValidationConf.scala | 10 +- .../changevalidation/ChangeRequestJson.scala | 25 +++ .../WorkflowJdbcRepository.scala | 29 +++- .../api/WorkflowInternalApi.scala | 142 ++++++++++++++++++ .../api_workflowinternal.yml | 19 +++ .../changevalidation/MockServices.scala | 8 + .../WorkflowJdbcRepositoryTest.scala | 78 +++++++++- .../api/WorkflowInternalApiTest.scala | 122 +++++++++++++++ 8 files changed, 425 insertions(+), 8 deletions(-) create mode 100644 change-validation/src/main/scala/com/normation/plugins/changevalidation/api/WorkflowInternalApi.scala create mode 100644 change-validation/src/test/resources/changevalidation_api/api_workflowinternal.yml create mode 100644 change-validation/src/test/scala/com/normation/plugins/changevalidation/api/WorkflowInternalApiTest.scala 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 c6bddd710..f3c0e4d93 100644 --- a/change-validation/src/main/scala/bootstrap/rudder/plugin/ChangeValidationConf.scala +++ b/change-validation/src/main/scala/bootstrap/rudder/plugin/ChangeValidationConf.scala @@ -71,6 +71,8 @@ import com.normation.plugins.changevalidation.api.SupervisedTargetsApi import com.normation.plugins.changevalidation.api.SupervisedTargetsApiImpl import com.normation.plugins.changevalidation.api.ValidatedUserApi import com.normation.plugins.changevalidation.api.ValidatedUserApiImpl +import com.normation.plugins.changevalidation.api.WorkflowInternalApi +import com.normation.plugins.changevalidation.api.WorkflowInternalApiImpl import com.normation.plugins.changevalidation.extension.ChangeValidationTab import com.normation.rudder.domain.nodes.NodeGroupId import com.normation.rudder.domain.policies.DirectiveUid @@ -319,14 +321,18 @@ object ChangeValidationConf extends RudderPluginModule { roValidatedUserRepository, woValidatedUserRepository ) + val api4 = new WorkflowInternalApiImpl( + roWorkflowRepository, + RudderConfig.userService + ) new LiftApiModuleProvider[EndpointSchema] { override def schemas: ApiModuleProvider[EndpointSchema] = new ApiModuleProvider[EndpointSchema] { override def endpoints: List[EndpointSchema] = - ValidatedUserApi.endpoints ::: SupervisedTargetsApi.endpoints ::: ChangeRequestApi.endpoints + ValidatedUserApi.endpoints ::: SupervisedTargetsApi.endpoints ::: ChangeRequestApi.endpoints ::: WorkflowInternalApi.endpoints } override def getLiftEndpoints(): List[LiftApiModule] = - api1.getLiftEndpoints() ::: api2.getLiftEndpoints() ::: api3.getLiftEndpoints() + api1.getLiftEndpoints() ::: api2.getLiftEndpoints() ::: api3.getLiftEndpoints() ::: api4.getLiftEndpoints() } } diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJson.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJson.scala index 1e565d3c9..4c01afb06 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJson.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJson.scala @@ -198,6 +198,31 @@ object ChangeRequestJson { } } +/** + * Class that represents the number of change requests that are currently in a "pending" status, i.e. + * "Pending validation" and "Pending deployment" respectively. + * Both fields are optional : either field will be present if and only if the user who made the request has + * the required authorization type, i.e. Validator.Read, and Deployer.Read authorizations respectively. + * + * @param pendingValidation the current number of change requests that have the "Pending validation" status + * @param pendingDeployment the current number of change requests that have the "Pending deployment" status + */ +final case class PendingCountJson( + pendingValidation: Option[Long], + pendingDeployment: Option[Long] +) + +object PendingCountJson { + implicit val encoder: JsonEncoder[PendingCountJson] = DeriveJsonEncoder.gen[PendingCountJson] + + def from(map: Map[WorkflowNodeId, Long]): PendingCountJson = { + PendingCountJson( + map.get(TwoValidationStepsWorkflowServiceImpl.Validation.id), + map.get(TwoValidationStepsWorkflowServiceImpl.Deployment.id) + ) + } +} + @jsonDiscriminator("action") sealed trait ActionChangeJson { def name: String = this match { case ActionChangeJson.create => "create" diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/WorkflowJdbcRepository.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/WorkflowJdbcRepository.scala index 4562a2b29..878592818 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/WorkflowJdbcRepository.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/WorkflowJdbcRepository.scala @@ -35,7 +35,7 @@ ************************************************************************************* */ package com.normation.plugins.changevalidation - +import cats.data.NonEmptyList import cats.implicits.* import com.normation.errors.IOResult import com.normation.rudder.db.Doobie @@ -43,6 +43,7 @@ import com.normation.rudder.domain.workflows.ChangeRequestId import com.normation.rudder.domain.workflows.WorkflowNodeId import doobie.* import doobie.implicits.* +import doobie.util.fragments import net.liftweb.common.Loggable import zio.interop.catz.* @@ -52,6 +53,8 @@ import zio.interop.catz.* trait RoWorkflowRepository { def getAllByState(state: WorkflowNodeId): IOResult[Seq[ChangeRequestId]] + def getCountByState(filter: NonEmptyList[WorkflowNodeId]): IOResult[Map[WorkflowNodeId, Long]] + def getStateOfChangeRequest(crId: ChangeRequestId): IOResult[WorkflowNodeId] def getAllChangeRequestsState(): IOResult[Map[ChangeRequestId, WorkflowNodeId]] @@ -68,6 +71,12 @@ trait RoWorkflowJdbcRepositorySQL { sql"select id from workflow where state = $state".query[ChangeRequestId] } + def getCountByStateSQL(filter: NonEmptyList[WorkflowNodeId]): Query0[(WorkflowNodeId, Long)] = { + val f = fragments.in(fr"state", filter) + sql"select state,count(distinct(id)) from workflow where ${f} group by state" + .query[(WorkflowNodeId, Long)] + } + def getStateOfChangeRequestSQL(crId: ChangeRequestId): Query0[WorkflowNodeId] = { sql"select state from workflow where id = $crId".query[WorkflowNodeId] } @@ -98,6 +107,24 @@ class RoWorkflowJdbcRepository(doobie: Doobie) extends RoWorkflowRepository with ) } + /** + * Returns the number of change requests for each state in the given filter. + * If there are no existing change requests for a given state in the filter, the count for this state will be 0. + * @param filter + * @return + */ + override def getCountByState(filter: NonEmptyList[WorkflowNodeId]): IOResult[Map[WorkflowNodeId, Long]] = { + transactIOResult("Could not get total count of change requests in each state")(xa => { + for { + req <- getCountByStateSQL(filter).to[Vector].transact(xa).map(_.toMap) + initMap = filter.map((_, 0L)).toList.toMap + res = req.combine(initMap) + } yield { + res + } + }) + } + def getStateOfChangeRequest(crId: ChangeRequestId): IOResult[WorkflowNodeId] = { transactIOResult(s"Could not get state of change request with id ${crId.value}")(xa => getStateOfChangeRequestSQL(crId).unique.transact(xa) diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/api/WorkflowInternalApi.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/api/WorkflowInternalApi.scala new file mode 100644 index 000000000..fb6e320e9 --- /dev/null +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/api/WorkflowInternalApi.scala @@ -0,0 +1,142 @@ +/* + ************************************************************************************* + * Copyright 2025 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.plugins.changevalidation.api + +import cats.data.NonEmptyList +import com.normation.plugins.changevalidation.PendingCountJson +import com.normation.plugins.changevalidation.RoWorkflowRepository +import com.normation.plugins.changevalidation.TwoValidationStepsWorkflowServiceImpl +import com.normation.rudder.AuthorizationType +import com.normation.rudder.api.ApiVersion +import com.normation.rudder.api.HttpAction.GET +import com.normation.rudder.rest.ApiModuleProvider +import com.normation.rudder.rest.ApiPath +import com.normation.rudder.rest.AuthzToken +import com.normation.rudder.rest.EndpointSchema +import com.normation.rudder.rest.EndpointSchema.syntax.AddPath +import com.normation.rudder.rest.EndpointSchema.syntax.BuildPath +import com.normation.rudder.rest.EndpointSchema0 +import com.normation.rudder.rest.InternalApi +import com.normation.rudder.rest.SortIndex +import com.normation.rudder.rest.StartsAtVersion21 +import com.normation.rudder.rest.ZeroParam +import com.normation.rudder.rest.implicits.ToLiftResponseOne +import com.normation.rudder.rest.lift.DefaultParams +import com.normation.rudder.rest.lift.LiftApiModule +import com.normation.rudder.rest.lift.LiftApiModule0 +import com.normation.rudder.rest.lift.LiftApiModuleProvider +import com.normation.rudder.users.UserService +import enumeratum.Enum +import enumeratum.EnumEntry +import net.liftweb.http.LiftResponse +import net.liftweb.http.Req +import sourcecode.Line +import zio.syntax.ToZio + +sealed trait WorkflowInternalApi extends EnumEntry with EndpointSchema with InternalApi with SortIndex +object WorkflowInternalApi extends Enum[WorkflowInternalApi] with ApiModuleProvider[WorkflowInternalApi] { + + final case object PendingChangeRequestCount extends WorkflowInternalApi with ZeroParam with StartsAtVersion21 with SortIndex { + val z = implicitly[Line].value + val (action, path) = GET / "changevalidation" / "workflow" / "pendingCountByStatus" + val description = + "Get total count of change requests in each state, i.e. PendingValidation and PendingDeployment" + + override def dataContainer: Option[String] = Some("workflow") + override def authz: List[AuthorizationType] = { + List(AuthorizationType.Deployer.Read, AuthorizationType.Validator.Read) + } + } + + override def endpoints: List[WorkflowInternalApi] = values.toList.sortBy(_.z) + override def values: IndexedSeq[WorkflowInternalApi] = findValues +} + +class WorkflowInternalApiImpl( + readWorkflow: RoWorkflowRepository, + userService: UserService +) extends LiftApiModuleProvider[WorkflowInternalApi] { + import com.normation.plugins.changevalidation.api.WorkflowInternalApi as API + + override def schemas: ApiModuleProvider[WorkflowInternalApi] = API + + override def getLiftEndpoints(): List[LiftApiModule] = { + API.endpoints.map { case API.PendingChangeRequestCount => PendingChangeRequestCount } + } + + object PendingChangeRequestCount extends LiftApiModule0 { + + override val schema: EndpointSchema0 = API.PendingChangeRequestCount + + override def process0( + version: ApiVersion, + path: ApiPath, + req: Req, + params: DefaultParams, + authzToken: AuthzToken + ): LiftResponse = { + + val user = userService.getCurrentUser + + val isValidator = user.checkRights(AuthorizationType.Validator.Read) + val isDeployer = user.checkRights(AuthorizationType.Deployer.Read) + + val filter = { + if (isValidator && isDeployer) { + List(TwoValidationStepsWorkflowServiceImpl.Validation.id, TwoValidationStepsWorkflowServiceImpl.Deployment.id) + } else if (isValidator) List(TwoValidationStepsWorkflowServiceImpl.Validation.id) + else if (isDeployer) List(TwoValidationStepsWorkflowServiceImpl.Deployment.id) + else List() + } + + NonEmptyList.fromList(filter) match { + case None => + // Should never happen : a request from a user that doesn't have either rights will not be processed here + PendingCountJson(None, None).succeed.toLiftResponseOne(params, schema, None) + case Some(nonEmptyFilter) => + readWorkflow + .getCountByState(nonEmptyFilter) + .map(PendingCountJson.from) + .chainError("Could not get pending change request count") + .toLiftResponseOne(params, schema, None) + } + + } + + } +} diff --git a/change-validation/src/test/resources/changevalidation_api/api_workflowinternal.yml b/change-validation/src/test/resources/changevalidation_api/api_workflowinternal.yml new file mode 100644 index 000000000..f100720b2 --- /dev/null +++ b/change-validation/src/test/resources/changevalidation_api/api_workflowinternal.yml @@ -0,0 +1,19 @@ +description: Get number of pending deployment and pending validation change requests +method: GET +url: /secure/api/changevalidation/workflow/pendingCountByStatus +headers: +response: + code: 200 + content: >- + { + "action": "pendingChangeRequestCount", + "result": "success", + "data": { + "workflow": [ + { + "pendingValidation": 2, + "pendingDeployment": 1 + } + ] + } + } \ No newline at end of file diff --git a/change-validation/src/test/scala/com/normation/plugins/changevalidation/MockServices.scala b/change-validation/src/test/scala/com/normation/plugins/changevalidation/MockServices.scala index 0a9e2f67e..29cba6eee 100644 --- a/change-validation/src/test/scala/com/normation/plugins/changevalidation/MockServices.scala +++ b/change-validation/src/test/scala/com/normation/plugins/changevalidation/MockServices.scala @@ -37,6 +37,7 @@ package com.normation.plugins.changevalidation import better.files.File +import cats.data.NonEmptyList import com.normation.errors.IOResult import com.normation.eventlog.EventActor import com.normation.eventlog.EventLog @@ -227,6 +228,13 @@ class MockServices(changeRequestsByStatus: Map[WorkflowNodeId, List[ChangeReques state.succeed } + override def getCountByState(filter: NonEmptyList[WorkflowNodeId]): IOResult[Map[WorkflowNodeId, Long]] = { + changeRequestsByStatus.flatMap { + case (k, v) => + filter.find(_ == k).map({ _ => (k, v.size.toLong) }) + }.succeed + } + override def getAllByState(state: WorkflowNodeId): IOResult[Seq[ChangeRequestId]] = { ??? } diff --git a/change-validation/src/test/scala/com/normation/plugins/changevalidation/WorkflowJdbcRepositoryTest.scala b/change-validation/src/test/scala/com/normation/plugins/changevalidation/WorkflowJdbcRepositoryTest.scala index 3c7c438aa..280480c8b 100644 --- a/change-validation/src/test/scala/com/normation/plugins/changevalidation/WorkflowJdbcRepositoryTest.scala +++ b/change-validation/src/test/scala/com/normation/plugins/changevalidation/WorkflowJdbcRepositoryTest.scala @@ -37,7 +37,12 @@ package com.normation.plugins.changevalidation +import cats.data.NonEmptyList import cats.effect.IO +import cats.syntax.apply.* +import com.normation.plugins.changevalidation.TwoValidationStepsWorkflowServiceImpl.Cancelled +import com.normation.plugins.changevalidation.TwoValidationStepsWorkflowServiceImpl.Deployment +import com.normation.plugins.changevalidation.TwoValidationStepsWorkflowServiceImpl.Validation import com.normation.rudder.db.DBCommon import com.normation.rudder.domain.workflows.ChangeRequestId import com.normation.rudder.domain.workflows.WorkflowNodeId @@ -58,9 +63,22 @@ class WorkflowJdbcRepositoryTest extends Specification with DBCommon with IOChec super.initDb() // initialize some change requests to setup change requests for workflow doobie.transactRunEither(xa => { - // id: 1 - sql"insert into ChangeRequest (name, description, creationTime, content, modificationId) values ('a change request', 'a change request description', '2023-01-01T00:00:00', '', '11111111-1111-1111-1111-111111111111')".update.run - .transact(xa) + ( + // id: 1 + sql"insert into ChangeRequest (name, description, creationTime, content, modificationId) values ('a change request', 'a change request description', '2023-01-01T00:00:00', '', '11111111-1111-1111-1111-111111111111')".update.run *> + + // insert a change request that has status "Pending validation" + sql"insert into ChangeRequest (name, description, creationTime, content, modificationId) values ('pendingValidation1', 'a change request description', '2025-01-01T00:00:00', '', '11111111-1111-1111-1111-111111111111')".update.run *> + + // insert two change requests that both have status "Pending deployment" + sql"insert into ChangeRequest (name, description, creationTime, content, modificationId) values ('pendingDeployment2', 'a change request description', '2025-01-01T00:00:00', '', '11111111-1111-1111-1111-111111111111')".update.run *> + sql"insert into ChangeRequest (name, description, creationTime, content, modificationId) values ('pendingDeployment3', 'a change request description', '2025-01-01T00:00:00', '', '11111111-1111-1111-1111-111111111111')".update.run *> + + // insert ids 2, 3 and 4 in the workflow table with their corresponding status + sql"insert into Workflow (id, state) values (2, 'Pending validation')".update.run *> + sql"insert into Workflow (id, state) values (3, 'Pending deployment')".update.run *> + sql"insert into Workflow (id, state) values (4, 'Pending deployment')".update.run + ).transact(xa) }) match { case Right(_) => () case Left(ex) => throw ex @@ -72,12 +90,16 @@ class WorkflowJdbcRepositoryTest extends Specification with DBCommon with IOChec private lazy val roWorkflowJdbcRepository = new RoWorkflowJdbcRepository(doobie) private lazy val woWorkflowJdbcRepository = new WoWorkflowJdbcRepository(doobie) - val changeRequestId = ChangeRequestId(1) + val changeRequestId = ChangeRequestId(1) + val changeRequestId2 = ChangeRequestId(2) + val changeRequestId3 = ChangeRequestId(3) + val changeRequestId4 = ChangeRequestId(4) "WorkflowJdbcRepository" should { "type-check queries" in { check(WorkflowJdbcRepositorySQL.getAllByStateSQL(WorkflowNodeId("foo"))) + check(WorkflowJdbcRepositorySQL.getCountByStateSQL(NonEmptyList.of(WorkflowNodeId("foo")))) check(WorkflowJdbcRepositorySQL.getStateOfChangeRequestSQL(changeRequestId)) check(WorkflowJdbcRepositorySQL.getAllChangeRequestsStateSQL) check(WorkflowJdbcRepositorySQL.createWorkflowSQL(changeRequestId, WorkflowNodeId("foo"))) @@ -107,8 +129,54 @@ class WorkflowJdbcRepositoryTest extends Specification with DBCommon with IOChec } "get all change requests state" in { - roWorkflowJdbcRepository.getAllChangeRequestsState().runNow must beEqualTo(Map(changeRequestId -> secondWorkflowNodeId)) + roWorkflowJdbcRepository.getAllChangeRequestsState().runNow must beEqualTo( + Map( + changeRequestId -> secondWorkflowNodeId, + changeRequestId2 -> Validation.id, + changeRequestId3 -> Deployment.id, + changeRequestId4 -> Deployment.id + ) + ) + } + + "get the count of pending change requests for " in { + + "both the 'Pending validation' and 'Pending deployment' states" in { + roWorkflowJdbcRepository.getCountByState(NonEmptyList.of(Validation.id, Deployment.id)).runNow must beEqualTo( + Map( + Validation.id -> 1, + Deployment.id -> 2 + ) + ) + } + + "the 'Pending validation' state" in { + roWorkflowJdbcRepository.getCountByState(NonEmptyList.of(Validation.id)).runNow must beEqualTo( + Map( + Validation.id -> 1 + ) + ) + } + + "the 'Pending deployment' state" in { + roWorkflowJdbcRepository.getCountByState(NonEmptyList.of(Deployment.id)).runNow must beEqualTo( + Map( + Deployment.id -> 2 + ) + ) + } } + "return 0 for the number of change requests in a given state if no change request is in that state" in { + + (woWorkflowJdbcRepository.updateState(changeRequestId2, Validation.id, Cancelled.id) *> + + roWorkflowJdbcRepository.getCountByState(NonEmptyList.of(Validation.id, Deployment.id))).runNow must beEqualTo( + Map( + Validation.id -> 0, + Deployment.id -> 2 + ) + ) + } } } diff --git a/change-validation/src/test/scala/com/normation/plugins/changevalidation/api/WorkflowInternalApiTest.scala b/change-validation/src/test/scala/com/normation/plugins/changevalidation/api/WorkflowInternalApiTest.scala new file mode 100644 index 000000000..93a35dd33 --- /dev/null +++ b/change-validation/src/test/scala/com/normation/plugins/changevalidation/api/WorkflowInternalApiTest.scala @@ -0,0 +1,122 @@ +/* + ************************************************************************************* + * Copyright 2025 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.plugins.changevalidation.api + +import better.files.File +import com.normation.errors.IOResult +import com.normation.errors.effectUioUnit +import com.normation.plugins.changevalidation.MockServices +import com.normation.plugins.changevalidation.TwoValidationStepsWorkflowServiceImpl.Cancelled +import com.normation.plugins.changevalidation.TwoValidationStepsWorkflowServiceImpl.Deployment +import com.normation.plugins.changevalidation.TwoValidationStepsWorkflowServiceImpl.Validation +import com.normation.rudder.api.ApiVersion +import com.normation.rudder.domain.workflows.ChangeRequest +import com.normation.rudder.domain.workflows.ChangeRequestId +import com.normation.rudder.domain.workflows.ChangeRequestInfo +import com.normation.rudder.domain.workflows.ConfigurationChangeRequest +import com.normation.rudder.rest.RestTestSetUp +import com.normation.rudder.rest.TraitTestApiFromYamlFiles +import java.nio.file.Files +import org.junit.runner.RunWith +import zio.Scope +import zio.ZIO +import zio.test.Spec +import zio.test.TestEnvironment +import zio.test.ZIOSpecDefault +import zio.test.junit.ZTestJUnitRunner + +@RunWith(classOf[ZTestJUnitRunner]) +class WorkflowInternalApiTest extends ZIOSpecDefault { + + private def mockChangeRequest(id: Int): ChangeRequest = { + ConfigurationChangeRequest( + ChangeRequestId(id), + None, + ChangeRequestInfo(s"cr ${id}", "mock change request"), + Map.empty, + Map.empty, + Map.empty, + Map.empty + ) + } + + val restTestSetUp = RestTestSetUp.newEnv + + val tmpDir: File = File(Files.createTempDirectory("rudder-test-")) + val yamlSourceDirectory = "changevalidation_api" + val yamlDestTmpDirectory = tmpDir / "templates" + + val mockServices = new MockServices( + Map( + Validation.id -> List(mockChangeRequest(1), mockChangeRequest(2)), + Deployment.id -> List(mockChangeRequest(3)), + Cancelled.id -> List(mockChangeRequest(4)) + ) + ) + + val modules = List( + new WorkflowInternalApiImpl( + mockServices.workflowRepository, + restTestSetUp.userService + ) + ) + + val apiVersions = ApiVersion(13, true) :: ApiVersion(14, false) :: Nil + + val (rudderApi, liftRules) = TraitTestApiFromYamlFiles.buildLiftRules(modules, apiVersions, None) + val transformations: Map[String, String => String] = Map() + + override def spec: Spec[TestEnvironment with Scope, Any] = { + suite("All REST tests defined in files") { + + for { + s <- TraitTestApiFromYamlFiles.doTest( + yamlSourceDirectory, + yamlDestTmpDirectory, + liftRules, + List("api_workflowinternal.yml"), + transformations + ) + _ <- effectUioUnit( + if (java.lang.System.getProperty("tests.clean.tmp") != "false") IOResult.attempt(restTestSetUp.cleanup()) + else ZIO.unit + ) + } yield s + } + } +} From cc0ed6d164cfa3b0f9d18387293fe3cd5d6a41c7 Mon Sep 17 00:00:00 2001 From: vhayaert Date: Tue, 22 Apr 2025 16:24:03 +0200 Subject: [PATCH 45/65] Fixes #26770: WoChangeRequestJdbcRepository#updateChangeRequest executes several transactions --- .../ChangeRequestJdbcRepository.scala | 51 ++++++++++--------- .../WorkflowJdbcRepository.scala | 33 ++++++++---- 2 files changed, 50 insertions(+), 34 deletions(-) diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepository.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepository.scala index 084ce6e3c..ae0dc7e1b 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepository.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepository.scala @@ -38,6 +38,9 @@ package com.normation.plugins.changevalidation import cats.data.NonEmptyList +import cats.syntax.applicative.* +import cats.syntax.applicativeError.* +import cats.syntax.functor.* import cats.syntax.reducible.* import com.normation.errors.* import com.normation.eventlog.EventActor @@ -236,16 +239,16 @@ class RoChangeRequestJdbcRepository( } class WoChangeRequestJdbcRepository( - doobie: Doobie, - mapper: ChangeRequestMapper, - roRepo: RoChangeRequestRepository -) extends WoChangeRequestRepository with Loggable with WoChangeRequestJdbcRepositorySQL { + doobie: Doobie, + override val changeRequestMapper: ChangeRequestMapper, + roRepo: RoChangeRequestRepository +) extends WoChangeRequestRepository with Loggable with WoChangeRequestJdbcRepositorySQL with RoChangeRequestJdbcRepositorySQL { import doobie.* // get the different part from a change request: name, description, content, modId private[this] def getAtom(cr: ChangeRequest): (Option[String], Option[String], Elem, Option[String]) = { - val xml = mapper.crcSerialiser.serialise(cr) + val xml = changeRequestMapper.crcSerialiser.serialise(cr) val name = cr.info.name match { case "" => None case x => Some(x) @@ -297,29 +300,31 @@ class WoChangeRequestJdbcRepository( } /** - * Update a change request. The change request must exists. + * Update a change request. The change request must exist. */ def updateChangeRequest(changeRequest: ChangeRequest, actor: EventActor, reason: Option[String]): IOResult[ChangeRequest] = { // no transaction between steps, because we don't actually use anything in the existing change request - - for { - _ <- roRepo - .get(changeRequest.id) - .notOptional(s"Cannot update non-existent Change Request with id ${changeRequest.id.value}") - .tapError(err => ChangeValidationLoggerPure.error(err.fullMsg)) - _ <- { - val (name, desc, xml, modId) = getAtom(changeRequest) - transactIOResult(s"Could not update the change request with id ${changeRequest.id} in database")(xa => - updateChangeRequestSQL(name, desc, xml, modId, changeRequest.id).run.transact(xa) - ) + val process = { + for { + exists <- getSQL(changeRequest.id).option + _ <- exists match { + case None => + val msg = + s"Change Request with id ${changeRequest.id.value} was not found in database" + new IllegalArgumentException(msg).raiseError[ConnectionIO, Unit] + case Some(_) => + val (name, desc, xml, modId) = getAtom(changeRequest) + updateChangeRequestSQL(name, desc, xml, modId, changeRequest.id).run.void + } + } yield { + changeRequest } - updated <- roRepo - .get(changeRequest.id) - .notOptional(s"Couldn't find the updated entry when updating Change Request ${changeRequest.id.value}") - .tapError(err => ChangeValidationLoggerPure.error(err.fullMsg)) - } yield { - updated } + + transactIOResult( + s"Could not update change request with id ${changeRequest.id.value} in database" + )(xa => process.transact(xa)) + .tapError(err => ChangeValidationLoggerPure.error(err.fullMsg)) } } diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/WorkflowJdbcRepository.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/WorkflowJdbcRepository.scala index 878592818..c1085d596 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/WorkflowJdbcRepository.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/WorkflowJdbcRepository.scala @@ -35,6 +35,7 @@ ************************************************************************************* */ package com.normation.plugins.changevalidation + import cats.data.NonEmptyList import cats.implicits.* import com.normation.errors.IOResult @@ -45,6 +46,7 @@ import doobie.* import doobie.implicits.* import doobie.util.fragments import net.liftweb.common.Loggable +import zio.ZIO import zio.interop.catz.* /** @@ -161,35 +163,44 @@ class WoWorkflowJdbcRepository(doobie: Doobie) extends WoWorkflowRepository with } def updateState(crId: ChangeRequestId, from: WorkflowNodeId, state: WorkflowNodeId): IOResult[WorkflowNodeId] = { - val process = { + val process: ConnectionIO[Either[String, Unit]] = { for { exists <- getStateOfChangeRequestSQL(crId).option - _ <- exists match { + update <- exists match { case Some(s) => if (s == from) { updateStateSQL( crId, from, state - ).run.attempt // swallows any "constraint violation" error if CrId is not in the ChangeRequest table + ).run.attempt.map( + _.bimap( + err => err.getMessage, + _ => () + ) + ) } else { - val msg = s"Cannot change status of ChangeRequest '${crId.value}': it has the status '${s.value}' " + - s"but we were expecting '${from.value}'. Perhaps someone else changed it concurrently?" - ChangeValidationLogger.error(msg) + val msg = s"Cannot change status of ChangeRequest '${crId.value}': it has status '${s.value}' " + + s"but the expected status was '${from.value}'. Perhaps someone else changed it concurrently?" Left(msg).pure[ConnectionIO] } case None => val msg = s"Cannot change a workflow for Change Request id ${crId.value}, as it is not part of any workflow yet" - ChangeValidationLogger.error(msg) Left(msg).pure[ConnectionIO] } } yield { - state + update } } - transactIOResult( - s"Could not update state of change request with id ${crId.value} from ${from.value} to ${state.value}" - )(xa => process.transact(xa)) + + for { + res <- transactIOResult( + s"Could not update state of change request with id ${crId.value} from ${from.value} to ${state.value}" + )(xa => process.transact(xa)) + _ <- ZIO.whenCase(res) { case Left(err) => ChangeValidationLoggerPure.error(err) } + } yield { + state + } } } From 902b5c246832bad6b508736f599e8fd6d920c7ec Mon Sep 17 00:00:00 2001 From: Raphael Gauthier Date: Thu, 24 Apr 2025 16:01:33 +0200 Subject: [PATCH 46/65] Fixes #26794: We cannot scroll to the bottom change validation tab --- .../main/resources/template/ChangeValidationManagement.html | 4 ++-- change-validation/src/main/style/change-validation.css | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/change-validation/src/main/resources/template/ChangeValidationManagement.html b/change-validation/src/main/resources/template/ChangeValidationManagement.html index 44577c3c1..25f03fcfc 100644 --- a/change-validation/src/main/resources/template/ChangeValidationManagement.html +++ b/change-validation/src/main/resources/template/ChangeValidationManagement.html @@ -9,7 +9,7 @@ -
    +
    -
    +
    diff --git a/change-validation/src/main/style/change-validation.css b/change-validation/src/main/style/change-validation.css index c0bd6d803..d2b095b31 100644 --- a/change-validation/src/main/style/change-validation.css +++ b/change-validation/src/main/style/change-validation.css @@ -47,9 +47,7 @@ flex-basis : initial !important; flex: initial !important; } -.rudder-template .one-col .template-main .main-details { - padding-bottom: 0px; -} + /* ========= */ ul.clipboard-list > li { padding: 6px 12px; From 463d7b865de4a2bc41a745b7be8ea6f72c640452 Mon Sep 17 00:00:00 2001 From: vhayaert Date: Wed, 23 Apr 2025 15:46:17 +0200 Subject: [PATCH 47/65] Fixes #26775: Refactoring : use ZIO instead of Box in change-validation snippets --- .../snippet/ChangeRequestChangesForm.scala | 15 +-- .../snippet/ChangeRequestDetails.scala | 3 +- .../snippet/ChangeRequestEditForm.scala | 9 +- .../snippet/ChangeValidationSettings.scala | 121 +++++++++++------- 4 files changed, 87 insertions(+), 61 deletions(-) diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestChangesForm.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestChangesForm.scala index 8857bd725..8cb86d8de 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestChangesForm.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestChangesForm.scala @@ -38,7 +38,6 @@ package com.normation.plugins.changevalidation.snippet import bootstrap.liftweb.* -import com.normation.box.* import com.normation.cfclerk.domain.SectionSpec import com.normation.cfclerk.domain.TechniqueId import com.normation.cfclerk.domain.TechniqueName @@ -627,7 +626,7 @@ class ChangeRequestChangesForm( (ruleChange.change.map { change => val rule = change.diff.rule (for { - groupLib <- getGroupLib().toBox + groupLib <- getGroupLib() } yield { change.diff match { @@ -645,13 +644,11 @@ class ChangeRequestChangesForm( displayRule(rule, rootRuleCategory, groupLib) } - }) match { - case Full(xml) => - xml - case eb: EmptyBox => - val fail = eb ?~! s"Could not display diff for ${rule.name} (${rule.id.serialize})" - logger.error(fail.messageChain) -
    {fail.messageChain}
    + }).chainError(s"Could not display diff for ${rule.name} (${rule.id.serialize})").either.runNow match { + case Right(xml) => xml + case Left(err) => + logger.error(err.fullMsg) +
    {err.fullMsg}
    } }) match { case Full(xml) => diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestDetails.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestDetails.scala index 4035f5182..4d8b0a6c9 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestDetails.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestDetails.scala @@ -40,6 +40,7 @@ package com.normation.plugins.changevalidation.snippet import bootstrap.liftweb.RudderConfig import bootstrap.rudder.plugin.ChangeValidationConf import com.normation.box.* +import com.normation.errors.BoxToIO import com.normation.errors.IOResult import com.normation.eventlog.EventActor import com.normation.plugins.changevalidation.ChangeValidationLogger @@ -147,7 +148,7 @@ class ChangeRequestDetails extends DispatchSnippet with Loggable { new ChangeRequestEditForm( cr.info, cr.owner, - step, + step.toIO, cr.id, changeDetailsCallback(cr)(_) ).display diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestEditForm.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestEditForm.scala index ebd07cbb2..42c9ab494 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestEditForm.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestEditForm.scala @@ -38,11 +38,13 @@ package com.normation.plugins.changevalidation.snippet import bootstrap.liftweb.RudderConfig +import com.normation.errors.IOResult import com.normation.rudder.ActionType import com.normation.rudder.domain.workflows.* import com.normation.rudder.users.CurrentUser import com.normation.rudder.web.ChooseTemplate import com.normation.rudder.web.model.* +import com.normation.zio.UnsafeRun import net.liftweb.common.* import net.liftweb.http.* import net.liftweb.http.js.* @@ -61,7 +63,7 @@ object ChangeRequestEditForm { class ChangeRequestEditForm( var info: ChangeRequestInfo, creator: String, - step: Box[WorkflowNodeId], + step: IOResult[WorkflowNodeId], crId: ChangeRequestId, SuccessCallback: ChangeRequestInfo => JsCmd ) extends DispatchSnippet with Loggable { @@ -100,7 +102,7 @@ class ChangeRequestEditForm( val authz = CurrentUser.getRights.authorizationTypes.toSeq.collect { case right: ActionType.Edit => right.authzKind } val isOwner = creator == CurrentUser.actor.name step.map(workflowService.isEditable(authz, _, isOwner)) - }.openOr(false) + }.orElseSucceed(false).runNow private[this] def actionButton = { if (isEditable) @@ -134,7 +136,8 @@ class ChangeRequestEditForm( "#CRId *" #> crId.value & "#CRStatusDetails *" #> step .map(wfId => Text(wfId.toString)) - .openOr(
    Cannot find the status of this change request
    ) & + .orElseSucceed(
    Cannot find the status of this change request
    ) + .runNow & "#CRDescription *" #> CRDescription & "#CRSave *" #> actionButton)(form) } diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeValidationSettings.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeValidationSettings.scala index 75fb5d44b..0e97c4771 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeValidationSettings.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeValidationSettings.scala @@ -40,13 +40,13 @@ package com.normation.plugins.changevalidation.snippet import bootstrap.liftweb.RudderConfig import com.normation.appconfig.ReadConfigService import com.normation.appconfig.UpdateConfigService -import com.normation.box.* -import net.liftweb.common.* +import com.normation.zio.UnsafeRun import net.liftweb.http.* import net.liftweb.http.js.JsCmds.Run import net.liftweb.http.js.JsCmds.Script import net.liftweb.util.Helpers.* import scala.xml.NodeSeq +import zio.syntax.* class ChangeValidationSettings extends DispatchSnippet { @@ -60,26 +60,38 @@ class ChangeValidationSettings extends DispatchSnippet { def workflowConfiguration: NodeSeq => NodeSeq = { (xml: NodeSeq) => // initial values, updated on successful submit - var initEnabled = configService.rudder_workflow_enabled().toBox - var initSelfVal = configService.rudder_workflow_self_validation().toBox - var initSelfDep = configService.rudder_workflow_self_deployment().toBox + var initEnabled = configService.rudder_workflow_enabled() + var initSelfVal = configService.rudder_workflow_self_validation() + var initSelfDep = configService.rudder_workflow_self_deployment() // form values - var enabled = initEnabled.getOrElse(false) - var selfVal = initSelfVal.getOrElse(false) - var selfDep = initSelfDep.getOrElse(false) + var enabled = initEnabled.orElseSucceed(false).runNow + var selfVal = initSelfVal.orElseSucceed(false).runNow + var selfDep = initSelfDep.orElseSucceed(false).runNow def submit = { - configService.set_rudder_workflow_enabled(enabled).toBox.foreach(updateOk => initEnabled = Full(enabled)) - configService.set_rudder_workflow_self_validation(selfVal).toBox.foreach(updateOk => initSelfVal = Full(selfVal)) - configService.set_rudder_workflow_self_deployment(selfDep).toBox.foreach(updateOk => initSelfDep = Full(selfDep)) + configService.set_rudder_workflow_enabled(enabled).either.runNow match { + case Right(_) => initEnabled = enabled.succeed + case _ => () + } + + configService.set_rudder_workflow_self_validation(selfVal).either.runNow match { + case Right(_) => initSelfVal = selfVal.succeed + case _ => () + } + + configService.set_rudder_workflow_self_deployment(selfDep).either.runNow match { + case Right(_) => initSelfDep = selfDep.succeed + case _ => () + } + S.notice("updateWorkflow", "Change Requests (validation workflow) configuration correctly updated") check() } - def noModif = (initEnabled.map(_ == enabled).getOrElse(false) - && initSelfVal.map(_ == selfVal).getOrElse(false) - && initSelfDep.map(_ == selfDep).getOrElse(false)) + def noModif = (initEnabled.map(_ == enabled).orElseSucceed(false).runNow + && initSelfVal.map(_ == selfVal).orElseSucceed(false).runNow + && initSelfDep.map(_ == selfDep).orElseSucceed(false).runNow) def check() = { if (!noModif) { @@ -113,67 +125,75 @@ class ChangeValidationSettings extends DispatchSnippet { } ("#workflowEnabled" #> { - initEnabled match { - case Full(value) => + initEnabled + .chainError("there was an error while fetching value of property: 'Enable Change Requests' ") + .either + .runNow match { + case Right(value) => SHtml.ajaxCheckbox( value, initJs _, ("id", "workflowEnabled"), ("class", "twoCol") ) - case eb: EmptyBox => - val fail = eb ?~ "there was an error, while fetching value of property: 'Enable Change Requests' " -
    {fail.msg}
    + case Left(err) => +
    {err.msg}
    } } & "#selfVal" #> { - initSelfVal match { - case Full(value) => + initSelfVal + .chainError("there was an error while fetching value of property: 'Allow self validation' ") + .either + .runNow match { + case Right(value) => SHtml.ajaxCheckbox( value, (b: Boolean) => { selfVal = b; check() }, ("id", "selfVal"), ("class", "twoCol") ) - case eb: EmptyBox => - val fail = eb ?~ "there was an error, while fetching value of property: 'Allow self validation' " -
    {fail.msg}
    + case Left(err) => +
    {err.msg}
    } - } & "#selfDep " #> { - initSelfDep match { - case Full(value) => + initSelfDep + .chainError("there was an error while fetching value of property: 'Allow self deployment' ") + .either + .runNow match { + case Right(value) => SHtml.ajaxCheckbox( value, (b: Boolean) => { selfDep = b; check() }, ("id", "selfDep"), ("class", "twoCol") ) - case eb: EmptyBox => - val fail = eb ?~ "there was an error, while fetching value of property: 'Allow self deployment' " -
    {fail.msg}
    + case Left(err) => +
    {err.msg}
    } } & "#selfValTooltip *" #> { - initSelfVal match { - case Full(_) => - val tooltipMsg = """Allow users to validate Change Requests they created themselves? Validating is moving a Change Request to the "Pending deployment" status""" + initSelfVal.either.runNow match { + case Right(_) => + val tooltipMsg = + """Allow users to validate Change Requests they created themselves? Validating is moving a Change Request to the "Pending deployment" status""" - case _ => NodeSeq.Empty + case Left(_) => NodeSeq.Empty } } & "#selfDepTooltip *" #> { - initSelfDep match { - case Full(_) => - val tooltipMsg = """Allow users to deploy Change Requests they created themselves? Deploying is effectively applying a Change Request in the "Pending deployment" status.""" + + initSelfDep.either.runNow match { + case Right(_) => + val tooltipMsg = + """Allow users to deploy Change Requests they created themselves? Deploying is effectively applying a Change Request in the "Pending deployment" status.""" - case _ => NodeSeq.Empty + case Left(_) => NodeSeq.Empty } } & @@ -185,21 +205,24 @@ class ChangeValidationSettings extends DispatchSnippet { // same as workflowConfiguration but with 1 single checkbox, and val autoValidatedUsers = configService.rudder_workflow_validation_auto_validated_users().toBox def validationConfiguration: NodeSeq => NodeSeq = { (xml: NodeSeq) => // initial value, updated on successful submit - var initAutoValidatedUsers = configService.rudder_workflow_validate_all().toBox + var initAutoValidatedUsers = configService.rudder_workflow_validate_all() // form value - var autoValidatedUsers = initAutoValidatedUsers.getOrElse(false) + var autoValidatedUsers = initAutoValidatedUsers.orElseSucceed(false).runNow def submit = { configService .set_rudder_workflow_validate_all(autoValidatedUsers) - .toBox - .foreach(updateOk => initAutoValidatedUsers = Full(autoValidatedUsers)) + .either + .runNow match { + case Right(_) => initAutoValidatedUsers = autoValidatedUsers.succeed + case _ => () + } S.notice("updateValidation", "Validation configuration correctly updated") check() } - def noModif = initAutoValidatedUsers.map(_ == autoValidatedUsers).getOrElse(false) + def noModif = initAutoValidatedUsers.map(_ == autoValidatedUsers).orElseSucceed(false).runNow def check() = { if (!noModif) { @@ -209,17 +232,19 @@ class ChangeValidationSettings extends DispatchSnippet { } ("#validationAutoValidatedUser" #> { - initAutoValidatedUsers match { - case Full(value) => + initAutoValidatedUsers + .chainError("there was an error while fetching value of property: 'Auto validated users' ") + .either + .runNow match { + case Right(value) => SHtml.ajaxCheckbox( value, (b: Boolean) => { autoValidatedUsers = b; check() }, ("id", "validationAutoValidatedUser"), ("class", "twoCol") ) - case eb: EmptyBox => - val fail = eb ?~ "there was an error, while fetching value of property: 'Auto validated users' " -
    {fail.msg}
    + case Left(err) => +
    {err.msg}
    } } & "#validationAutoSubmit " #> { From ed48cf63d366dcb54fd3765352b43026cd483aed Mon Sep 17 00:00:00 2001 From: vhayaert Date: Tue, 29 Apr 2025 14:48:40 +0200 Subject: [PATCH 48/65] Fixes #26802: Change request JDBC repo tests has the wrong error type in tests --- .../changevalidation/ChangeRequestJdbcRepositoryTest.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/change-validation/src/test/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepositoryTest.scala b/change-validation/src/test/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepositoryTest.scala index dfce81474..25707df14 100644 --- a/change-validation/src/test/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepositoryTest.scala +++ b/change-validation/src/test/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepositoryTest.scala @@ -44,7 +44,8 @@ import com.normation.BoxSpecMatcher import com.normation.GitVersion import com.normation.cfclerk.domain.TechniqueName import com.normation.cfclerk.domain.TechniqueVersionHelper -import com.normation.errors.Inconsistency +import com.normation.errors.RudderError +import com.normation.errors.SystemError import com.normation.eventlog.EventActor import com.normation.eventlog.ModificationId import com.normation.rudder.db.DBCommon @@ -358,7 +359,9 @@ class ChangeRequestJdbcRepositoryTest extends Specification with DBCommon with I Some("reason") ) .either - .runNow must beLeft(Inconsistency(s"Cannot update non-existent Change Request with id 999")) + .runNow must beLeft(beLike[RudderError] { + case err: SystemError => err.msg must beEqualTo("Could not update change request with id 999 in database") + }) } } From bde8088e309a10213a2aec6a6c6d4b8fa7cdebb1 Mon Sep 17 00:00:00 2001 From: vhayaert Date: Thu, 24 Apr 2025 16:49:11 +0200 Subject: [PATCH 49/65] Fixes #26788: Rewrite WorkflowInformation navigation bar in Elm --- change-validation/src/main/elm/elm.json | 4 +- .../src/main/elm/sources/ErrorMessages.elm | 26 ++ .../src/main/elm/sources/Init.elm | 40 +-- .../src/main/elm/sources/Notifications.elm | 11 + .../main/elm/sources/SupervisedTargets.elm | 16 +- .../main/elm/sources/WorkflowInformation.elm | 310 ++++++++++++++++++ .../elm/sources/WorkflowInformationTest.elm | 88 +++++ .../src/main/elm/sources/WorkflowUsers.elm | 4 +- .../comet/WorkflowInformation.scala | 183 ++++------- 9 files changed, 521 insertions(+), 161 deletions(-) create mode 100644 change-validation/src/main/elm/sources/ErrorMessages.elm create mode 100644 change-validation/src/main/elm/sources/Notifications.elm create mode 100644 change-validation/src/main/elm/sources/WorkflowInformation.elm create mode 100644 change-validation/src/main/elm/sources/WorkflowInformationTest.elm diff --git a/change-validation/src/main/elm/elm.json b/change-validation/src/main/elm/elm.json index df468af9a..70a0dd8e6 100644 --- a/change-validation/src/main/elm/elm.json +++ b/change-validation/src/main/elm/elm.json @@ -12,11 +12,13 @@ "elm/html": "1.0.0", "elm/http": "2.0.0", "elm/json": "1.1.3", - "elm/regex": "1.0.0" + "elm/regex": "1.0.0", + "elm-explorations/test": "2.2.0" }, "indirect": { "elm/bytes": "1.0.8", "elm/file": "1.0.5", + "elm/random": "1.0.0", "elm/time": "1.0.0", "elm/url": "1.0.0", "elm/virtual-dom": "1.0.3" diff --git a/change-validation/src/main/elm/sources/ErrorMessages.elm b/change-validation/src/main/elm/sources/ErrorMessages.elm new file mode 100644 index 000000000..81fe1a4a3 --- /dev/null +++ b/change-validation/src/main/elm/sources/ErrorMessages.elm @@ -0,0 +1,26 @@ +module ErrorMessages exposing (..) + +import Http + + +getErrorMessage : Http.Error -> String +getErrorMessage e = + let + errMessage = + case e of + Http.BadStatus status -> + "Code " ++ String.fromInt status + + Http.BadUrl str -> + "Invalid API url" + + Http.Timeout -> + "It took too long to get a response" + + Http.NetworkError -> + "Network error" + + Http.BadBody c -> + "Wrong content in request body" ++ c + in + errMessage diff --git a/change-validation/src/main/elm/sources/Init.elm b/change-validation/src/main/elm/sources/Init.elm index 428de21b4..8d228c53e 100644 --- a/change-validation/src/main/elm/sources/Init.elm +++ b/change-validation/src/main/elm/sources/Init.elm @@ -1,15 +1,6 @@ -port module Init exposing (..) +module Init exposing (..) import DataTypes exposing (EditMod(..), Model, Msg(..)) -import Http - ------------------------------- --- PORTS ------------------------------- - - -port successNotification : String -> Cmd msg -port errorNotification : String -> Cmd msg @@ -20,30 +11,9 @@ port errorNotification : String -> Cmd msg subscriptions : Model -> Sub Msg subscriptions _ = - Sub.none + Sub.none + initModel : String -> Model -initModel contextPath = - Model contextPath [] [] [] [] [] [] Off - -getErrorMessage : Http.Error -> String -getErrorMessage e = - let - errMessage = - case e of - Http.BadStatus status -> - "Code " ++ String.fromInt status - - Http.BadUrl str -> - "Invalid API url" - - Http.Timeout -> - "It took too long to get a response" - - Http.NetworkError -> - "Network error" - Http.BadBody c-> - "Wrong content in request body" ++ c - - in - errMessage \ No newline at end of file +initModel contextPath = + Model contextPath [] [] [] [] [] [] Off diff --git a/change-validation/src/main/elm/sources/Notifications.elm b/change-validation/src/main/elm/sources/Notifications.elm new file mode 100644 index 000000000..b3494f80e --- /dev/null +++ b/change-validation/src/main/elm/sources/Notifications.elm @@ -0,0 +1,11 @@ +port module Notifications exposing (..) + +------------------------------ +-- PORTS +------------------------------ + + +port successNotification : String -> Cmd msg + + +port errorNotification : String -> Cmd msg diff --git a/change-validation/src/main/elm/sources/SupervisedTargets.elm b/change-validation/src/main/elm/sources/SupervisedTargets.elm index f9247984e..2dc7198d9 100644 --- a/change-validation/src/main/elm/sources/SupervisedTargets.elm +++ b/change-validation/src/main/elm/sources/SupervisedTargets.elm @@ -1,16 +1,17 @@ module SupervisedTargets exposing (Category, Model, Msg(..), Subcategories(..), Target, alphanumericRegex, decodeApiCategory, decodeApiSave, decodeCategory, decodeSubcategories, decodeTarget, displayCategory, displaySubcategories, displayTarget, encodeTargets, getSupervisedIds, getTargets, init, isAlphanumeric, main, saveTargets, subscriptions, update, updateTarget, view) -import Html exposing (..) import Browser +import ErrorMessages exposing (getErrorMessage) +import Html exposing (..) import Html.Attributes exposing (checked, class, type_) import Html.Events exposing (..) import Http exposing (..) import Json.Decode as D exposing (Decoder) import Json.Decode.Pipeline exposing (..) import Json.Encode as E +import Notifications exposing (errorNotification, successNotification) import Regex import String -import Init exposing (errorNotification, successNotification, getErrorMessage) @@ -86,10 +87,10 @@ type Msg | SaveTargets (Result Error String) -- here the string is just the status message | SendSave | UpdateTarget Target - -- NOTIFICATIONS +-- NOTIFICATIONS ------------------------------ -- API -- ------------------------------ @@ -108,7 +109,7 @@ getTargets model = req = request { method = "GET" - , headers = [Http.header "X-Requested-With" "XMLHttpRequest"] + , headers = [ Http.header "X-Requested-With" "XMLHttpRequest" ] , url = url , body = emptyBody , expect = expectJson GetTargets decodeApiCategory @@ -116,7 +117,7 @@ getTargets model = , tracker = Nothing } in - req + req @@ -129,7 +130,7 @@ saveTargets model = req = request { method = "POST" - , headers = [Http.header "X-Requested-With" "XMLHttpRequest"] + , headers = [ Http.header "X-Requested-With" "XMLHttpRequest" ] , url = model.contextPath ++ "/secure/api/changevalidation/supervised/targets" , body = jsonBody (encodeTargets (getSupervisedIds model.allTargets)) , expect = expectJson SaveTargets decodeApiSave @@ -210,7 +211,7 @@ decodeTarget = encodeTargets : List String -> E.Value encodeTargets targets = - E.object [ ( "supervised", E.list (\s -> E.string s) targets ) ] + E.object [ ( "supervised", E.list (\s -> E.string s) targets ) ] @@ -368,6 +369,7 @@ displayTarget target = ] + ------------------------------ -- HELPERS ------------------------------ diff --git a/change-validation/src/main/elm/sources/WorkflowInformation.elm b/change-validation/src/main/elm/sources/WorkflowInformation.elm new file mode 100644 index 000000000..407c57ba5 --- /dev/null +++ b/change-validation/src/main/elm/sources/WorkflowInformation.elm @@ -0,0 +1,310 @@ +module WorkflowInformation exposing (..) + +import Browser +import ErrorMessages exposing (getErrorMessage) +import Html exposing (Html, a, i, li, span, text, ul) +import Html.Attributes as Attr +import Http exposing (Error, emptyBody, expectJson, header, request) +import Json.Decode exposing (Decoder, at, bool, field, index, int, list, map2, maybe) +import Notifications exposing (errorNotification) + + + +------------------------------ +-- Init and main -- +------------------------------ + + +getApiUrl : Model -> String -> String +getApiUrl m url = + m.contextPath ++ "/secure/api/" ++ url + + +getPendingValidationUrl : Model -> String +getPendingValidationUrl m = + m.contextPath ++ "/secure/configurationManager/changes/changeRequests/Pending_validation" + + +getPendingDeploymentUrl : Model -> String +getPendingDeploymentUrl m = + m.contextPath ++ "/secure/configurationManager/changes/changeRequests/Pending_deployment" + + +init : { contextPath : String } -> ( Model, Cmd Msg ) +init flags = + let + initModel = + Model flags.contextPath NotSet + in + ( initModel, getWorkflowEnabledSetting initModel ) + + +main = + Browser.element + { init = init + , view = view + , update = update + , subscriptions = subscriptions + } + + + +------------------------------ +-- MODEL -- +------------------------------ + + +type alias PendingCount = + { pendingValidation : Maybe Int + , pendingDeployment : Maybe Int + , totalCount : Int + } + + +type PendingCountOpt + = NotSet + | PendingCountWithTotal PendingCount + + +type WorkflowInfoStatus + = Enabled + | Disabled + + +type alias Model = + { contextPath : String + , pendingCount : PendingCountOpt + } + + +type Msg + = GetPendingCount (Result Error PendingCountOpt) + | GetWorkflowEnabledSetting (Result Error WorkflowInfoStatus) + + + +------------------------------ +-- API -- +------------------------------ +-- API call to get the number of pending change request for each respective "pending" status + + +getPendingCount : Model -> Cmd Msg +getPendingCount model = + let + req = + request + { method = "GET" + , headers = [ header "X-Requested-With" "XMLHttpRequest" ] + , url = getApiUrl model "changevalidation/workflow/pendingCountByStatus" + , body = emptyBody + , expect = expectJson GetPendingCount decodePendingCountList + , timeout = Nothing + , tracker = Nothing + } + in + req + + +getWorkflowEnabledSetting : Model -> Cmd Msg +getWorkflowEnabledSetting model = + let + req = + request + { method = "GET" + , headers = [ header "X-Requested-With" "XMLHttpRequest" ] + , url = getApiUrl model "settings/enable_change_request" + , body = emptyBody + , expect = expectJson GetWorkflowEnabledSetting decodeEnabledSetting + , timeout = Nothing + , tracker = Nothing + } + in + req + + + +------------------------------ +-- ENCODE / DECODE JSON -- +------------------------------ + + +pendingCountOpt : Maybe Int -> Maybe Int -> PendingCountOpt +pendingCountOpt pendingValidation pendingDeployment = + case ( pendingValidation, pendingDeployment ) of + ( Nothing, Nothing ) -> + NotSet + + -- At least one field (pendingValidation and/or pendingDeployment) is not equal to Nothing + ( _, _ ) -> + PendingCountWithTotal + { pendingDeployment = pendingDeployment + , pendingValidation = pendingValidation + , totalCount = Maybe.withDefault 0 pendingValidation + Maybe.withDefault 0 pendingDeployment + } + + +decodePendingCountOpt : Decoder PendingCountOpt +decodePendingCountOpt = + map2 + pendingCountOpt + (maybe (field "pendingValidation" int)) + (maybe (field "pendingDeployment" int)) + + +decodePendingCountList : Decoder PendingCountOpt +decodePendingCountList = + at [ "data" ] (field "workflow" (index 0 decodePendingCountOpt)) + + +decodeEnabledSetting : Decoder WorkflowInfoStatus +decodeEnabledSetting = + let + boolToStatus b = + case b of + True -> + Enabled + + False -> + Disabled + in + let + decSetting = + field "settings" (field "enable_change_request" (Json.Decode.map boolToStatus bool)) + in + at [ "data" ] decSetting + + + +------------------------------ +-- UPDATE -- +------------------------------ + + +update : Msg -> Model -> ( Model, Cmd Msg ) +update msg model = + case msg of + GetPendingCount result -> + case result of + Ok pendingCount -> + ( { model | pendingCount = pendingCount }, Cmd.none ) + + -- Unauthorized access + Err (Http.BadStatus 403) -> + ( { model | pendingCount = NotSet }, Cmd.none ) + + Err err -> + ( model, errorNotification ("Error while trying to fetch pending change requests: " ++ getErrorMessage err) ) + + GetWorkflowEnabledSetting result -> + case result of + Ok setting -> + case setting of + Enabled -> + ( model, getPendingCount model ) + + Disabled -> + ( { model | pendingCount = NotSet }, Cmd.none ) + + -- Unauthorized access + Err (Http.BadStatus 403) -> + ( { model | pendingCount = NotSet }, Cmd.none ) + + Err err -> + ( model, errorNotification ("Error while trying to fetch change_requests_enabled setting: " ++ getErrorMessage err) ) + + + +------------------------------ +-- VIEW -- +------------------------------ + + +view : Model -> Html Msg +view model = + case model.pendingCount of + PendingCountWithTotal pc -> + li + [ Attr.class "nav-item dropdown notifications-menu" + , Attr.id "workflow-app" + ] + [ a + [ Attr.href "#" + , Attr.class "dropdown-toggle" + , Attr.attribute "data-bs-toggle" "dropdown" + , Attr.attribute "role" "button" + , Attr.attribute "aria-expanded" "false" + ] + [ span [] + [ text "CR" ] + , viewDropdownToggle pc.totalCount + ] + , ul + [ Attr.class "dropdown-menu" + , Attr.attribute "role" "menu" + ] + [ li [] [ viewDropDownMenu model ] ] + ] + + NotSet -> + text "" + + +viewDropDownMenu : Model -> Html Msg +viewDropDownMenu model = + case model.pendingCount of + NotSet -> + ul [ Attr.class "menu" ] [] + + PendingCountWithTotal pc -> + ul [ Attr.class "menu" ] + [ displayPendingCount pc.pendingValidation "Pending Validation" (getPendingValidationUrl model) "pe-2 fa fa-flag-o" + , displayPendingCount pc.pendingDeployment "Pending Deployment" (getPendingDeploymentUrl model) "pe-2 fa fa-flag-checkered" + ] + + +viewDropdownToggle : Int -> Html Msg +viewDropdownToggle totalCount = + span + [ Attr.id "number" + , Attr.class "badge rudder-badge" + ] + [ Html.text (String.fromInt totalCount) ] + + +displayPendingCount : Maybe Int -> String -> String -> String -> Html Msg +displayPendingCount countOpt countName link flag = + case countOpt of + Nothing -> + Html.text "" + + Just count -> + li [] + [ a + [ Attr.href link + , Attr.class "pe-auto" + ] + [ span [] + [ i + [ Attr.class flag + ] + [] + , Html.text countName + ] + , span + [ Attr.class "float-end badge bg-light text-dark px-2" + ] + [ Html.text (String.fromInt count) ] + ] + ] + + + +------------------------------ +-- SUBSCRIPTIONS +------------------------------ + + +subscriptions : Model -> Sub Msg +subscriptions _ = + Sub.none diff --git a/change-validation/src/main/elm/sources/WorkflowInformationTest.elm b/change-validation/src/main/elm/sources/WorkflowInformationTest.elm new file mode 100644 index 000000000..586a3cf41 --- /dev/null +++ b/change-validation/src/main/elm/sources/WorkflowInformationTest.elm @@ -0,0 +1,88 @@ +module WorkflowInformationTest exposing (..) + +import Expect +import Fuzz exposing (int, maybe) +import Test exposing (..) +import WorkflowInformation exposing (PendingCountOpt(..), pendingCountOpt) + + +expectedTotalCount nonOptCount optCount = + case optCount of + Just res -> + res + nonOptCount + + Nothing -> + nonOptCount + + +suite = + describe "pending count" + [ -- A PendingCount Json that has two empty fields will always be decoded as a NotSet object + test "NotSet" <| + \_ -> + pendingCountOpt Nothing Nothing + |> Expect.equal NotSet + , -- A PendingCount Json whose pendingValidation field is present will always be decoded as a PendingCountWithTotal object + fuzz2 + int + (maybe int) + "Non-empty pendingValidation field" + <| + \pendingValidation pendingDeploymentOpt -> + pendingCountOpt (Just pendingValidation) pendingDeploymentOpt + |> Expect.equal + (PendingCountWithTotal + { pendingValidation = Just pendingValidation + , pendingDeployment = pendingDeploymentOpt + , totalCount = expectedTotalCount pendingValidation pendingDeploymentOpt + } + ) + , -- A PendingCount Json whose pendingDeployment field is present will always be decoded as a PendingCountWithTotal object + fuzz2 + (maybe int) + int + "Non-empty pendingDeployment field" + <| + \pendingValidationOpt pendingDeployment -> + pendingCountOpt pendingValidationOpt (Just pendingDeployment) + |> Expect.equal + (PendingCountWithTotal + { pendingValidation = pendingValidationOpt + , pendingDeployment = Just pendingDeployment + , totalCount = expectedTotalCount pendingDeployment pendingValidationOpt + } + ) + , -- A PendingCount Json whose pendingValidation and pendingDeployment fields are both present will always be decoded as a PendingCountWithTotal object + fuzz2 + int + int + "Non-empty pendingDeployment and pendingValidation fields" + <| + \pendingValidation pendingDeployment -> + pendingCountOpt (Just pendingValidation) (Just pendingDeployment) + |> Expect.equal + (PendingCountWithTotal + { pendingValidation = Just pendingValidation + , pendingDeployment = Just pendingDeployment + , totalCount = pendingValidation + pendingDeployment + } + ) + , -- After decoding, the obtained PendingCountOpt object must be a 'NotSet' if the two given fields are equal to 'Nothing', and be a 'PendingCountWithTotal' object whose fields are exactly equal to the given values. + fuzz2 + (maybe int) + (maybe int) + "PendingCountWithTotal cannot have two empty fields" + <| + \pendingValidation pendingDeployment -> + case pendingCountOpt pendingValidation pendingDeployment of + NotSet -> + ( pendingValidation, pendingDeployment ) |> Expect.equal ( Nothing, Nothing ) + + PendingCountWithTotal pendingCount -> + case ( pendingCount.pendingValidation, pendingCount.pendingDeployment ) of + ( Nothing, Nothing ) -> + Expect.fail "PendingCountWithTotal cannot have both of its pendingValidation and pendingDeployment fields be equal to 'Nothing'" + + ( _, _ ) -> + ( pendingCount.pendingValidation, pendingCount.pendingDeployment ) |> Expect.equal ( pendingValidation, pendingDeployment ) + ] diff --git a/change-validation/src/main/elm/sources/WorkflowUsers.elm b/change-validation/src/main/elm/sources/WorkflowUsers.elm index ba8346b0c..562844d03 100644 --- a/change-validation/src/main/elm/sources/WorkflowUsers.elm +++ b/change-validation/src/main/elm/sources/WorkflowUsers.elm @@ -3,8 +3,10 @@ module WorkflowUsers exposing (..) import ApiCalls exposing (getUsers) import Browser import DataTypes exposing (ColPos(..), EditMod(..), Model, Msg(..), User, UserList) -import Init exposing (initModel, subscriptions, errorNotification, successNotification, getErrorMessage) +import ErrorMessages exposing (getErrorMessage) +import Init exposing (initModel, subscriptions) import List exposing (filter, member) +import Notifications exposing (errorNotification, successNotification) import String import View exposing (view) diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/comet/WorkflowInformation.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/comet/WorkflowInformation.scala index 16abbb1e5..71f215135 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/comet/WorkflowInformation.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/comet/WorkflowInformation.scala @@ -38,158 +38,107 @@ package com.normation.plugins.changevalidation.comet import bootstrap.liftweb.RudderConfig -import com.normation.plugins.changevalidation.EitherWorkflowService -import com.normation.plugins.changevalidation.TwoValidationStepsWorkflowServiceImpl import com.normation.rudder.AuthorizationType import com.normation.rudder.batch.AsyncWorkflowInfo -import com.normation.rudder.services.workflows.WorkflowService import com.normation.rudder.services.workflows.WorkflowUpdate import com.normation.rudder.users.CurrentUser import com.normation.zio.UnsafeRun +import net.liftweb.common.Empty +import net.liftweb.common.Full import net.liftweb.common.Loggable import net.liftweb.http.* import scala.xml.* class WorkflowInformation extends CometActor with CometListener with Loggable { - private[this] def workflowService = { - RudderConfig.workflowLevelService.getWorkflowService() + + private[this] val asyncWorkflow = RudderConfig.asyncWorkflowInfo + + private[this] val isValidator = CurrentUser.checkRights(AuthorizationType.Validator.Read) + private[this] val isDeployer = CurrentUser.checkRights(AuthorizationType.Deployer.Read) + + private[this] var workflowEnabledPrev = getWorkflowEnabled() + private[this] var shouldLoadScript = workflowEnabledPrev + + /** A user must have either or both of the Validator.Read and Deployer.Read authorizations + * in order to display the pending change requests menu. */ + private[this] def hasRights() = isValidator || isDeployer + private[this] def getWorkflowEnabled() = { + if (hasRights()) RudderConfig.configService.rudder_workflow_enabled().orElseSucceed(false).runNow else false } - private[this] val asyncWorkflow = RudderConfig.asyncWorkflowInfo - private[this] val isValidator = CurrentUser.checkRights(AuthorizationType.Validator.Edit) - private[this] val isDeployer = CurrentUser.checkRights(AuthorizationType.Deployer.Edit) override def registerWith: AsyncWorkflowInfo = asyncWorkflow override val defaultHtml = NodeSeq.Empty - val layout = { - - } - def render = { - - val xml = RudderConfig.configService - .rudder_workflow_enabled() - .chainError("Error when trying to read Rudder configuration for workflow activation") - .either - .runNow match { - case Left(err) => - logger.error(err.fullMsg) - (".dropdown-menu *" #> ).apply(layout) - case Right(workflowEnabled) => - val cssSelect = { - if (workflowEnabled && (isValidator || isDeployer)) { - { - if (isValidator) pendingModifications - else ".dropdown-menu *+" #> NodeSeq.Empty - } & { - if (isDeployer) pendingDeployment - else ".dropdown-menu *+" #> NodeSeq.Empty - } & - "#number *" #> requestCount(workflowService) - } else { - ".dropdown *" #> NodeSeq.Empty - } - } - cssSelect(layout) + This menu is created by the WorkflowInformation Elm app. + */ + + val xml = { + if (shouldLoadScript) { + ("#workflow-app" #> + ).apply(
  • Error when trying to fetch pending change requests.

  • , - seq => { -
  • - - - - Pending review - - {seq.size} - -
  • - } - ) - .runNow - - case either: EitherWorkflowService => pendingModificationRec(either.current) - case _ => // For other kind of workflows, this has no meaning -
  • Error, the configured workflow does not have that step.

  • - } - } + override def lowPriority = { + case WorkflowUpdate => + if (!hasRights()) () - def pendingDeployment = { - val xml = pendingDeploymentRec(workflowService) + val workflowEnabled = getWorkflowEnabled() - "#workflow-app ul *+" #> xml - } + // The script should be loaded if the workflow_enabled setting has been enabled since the last render + if ((!workflowEnabledPrev) && workflowEnabled) shouldLoadScript = true + if (workflowEnabledPrev && (!workflowEnabled)) shouldLoadScript = false - private[this] def pendingDeploymentRec(workflowService: WorkflowService): NodeSeq = { - workflowService match { - case ws: TwoValidationStepsWorkflowServiceImpl => - ws.getItemsInStep(TwoValidationStepsWorkflowServiceImpl.Deployment.id) - .fold( - _ =>
  • Error when trying to fetch pending change requests.

  • , - seq => { -
  • - - - - Pending deployment - - {seq.size} - -
  • - } - ) - .runNow - case either: EitherWorkflowService => pendingDeploymentRec(either.current) - case _ => // For other kind of workflows, this has no meaning -
  • Error, the configured workflow does not have that step.

  • - } - } + workflowEnabledPrev = workflowEnabled - override def lowPriority = { case WorkflowUpdate => reRender() } + reRender() + } } From c98bf659c27682fab5d97ce8f6aec4a85e0c5f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Membr=C3=A9?= Date: Tue, 6 May 2025 20:27:08 +0200 Subject: [PATCH 50/65] Prepare next nightly branch of plugin --- main-build.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main-build.conf b/main-build.conf index e91e03123..5f2de0036 100644 --- a/main-build.conf +++ b/main-build.conf @@ -14,6 +14,6 @@ # Version of Rudder used to build the plugin. # It defined the API/ABI used and it is important for binary compatibility branch-type=next -rudder-version=8.3.1 +rudder-version=8.3.2 common-version=2.1.1 private-version=2.1.0 From d41ff9a0f89d2928a7b2ebc5b93eea499988cb25 Mon Sep 17 00:00:00 2001 From: vhayaert Date: Fri, 2 May 2025 15:10:38 +0200 Subject: [PATCH 51/65] Fixes #26857: Migration from Box to ZIO : Refactor remaining classes that use type Box in change-validation --- .../ChangeRequestJdbcRepository.scala | 6 ++- .../changevalidation/ChangeRequestJson.scala | 39 ++++++++++--------- .../api/ChangeRequestApi.scala | 1 - .../snippet/ChangeRequestChangesForm.scala | 14 +++---- .../snippet/ChangeRequestDetails.scala | 6 ++- .../snippet/ChangeRequestEditForm.scala | 2 +- .../snippet/ChangeRequestManagement.scala | 5 ++- 7 files changed, 40 insertions(+), 33 deletions(-) diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepository.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepository.scala index ae0dc7e1b..8e2a14024 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepository.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepository.scala @@ -61,7 +61,9 @@ import doobie.* import doobie.implicits.* import doobie.postgres.implicits.* import doobie.util.fragments -import net.liftweb.common.* +import net.liftweb.common.Box +import net.liftweb.common.EmptyBox +import net.liftweb.common.Full import net.liftweb.common.Loggable import org.joda.time.DateTime import scala.xml.Elem @@ -339,7 +341,7 @@ class ChangeRequestMapper( // unserialize the XML. // If it fails, produce a failure - // directives map is boxed because some Exception could be launched + // directives map is stored in a IOResult because an Exception could be launched def unserialize( id: Int, name: Option[String], diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJson.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJson.scala index 4c01afb06..c9dd17681 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJson.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJson.scala @@ -101,9 +101,6 @@ import io.scalaland.chimney.PartialTransformer import io.scalaland.chimney.Transformer import io.scalaland.chimney.partial.Result import io.scalaland.chimney.syntax.* -import net.liftweb.common.Empty -import net.liftweb.common.Failure -import net.liftweb.common.Full import scala.util.Try import zio.Chunk import zio.NonEmptyChunk @@ -439,10 +436,11 @@ object DirectiveChangeActionJson { case d: DirectiveDeleteChangeJson => DirectiveChangeActionJson(ActionChangeJson.delete, d) case d: DirectiveModifyChangeJson => DirectiveChangeActionJson(ActionChangeJson.modify, d) }) match { - case Empty => - Result.fromErrorString(s"Error while serializing directives from CR ${change.firstChange.diff.directive.id.serialize}") - case Failure(msg, exception, chain) => Result.fromErrorString(msg) - case Full(value) => value + case Left(err) => + Result.fromErrorString( + s"Error while serializing directives from CR ${change.firstChange.diff.directive.id.serialize}: ${err.msg}" + ) + case Right(value) => value } } @@ -570,10 +568,11 @@ object RuleChangeActionJson { case d: RuleDeleteChangeJson => RuleChangeActionJson(ActionChangeJson.delete, d) case d: RuleModifyChangeJson => RuleChangeActionJson(ActionChangeJson.modify, d) }) match { - case Empty => - Result.fromErrorString(s"Error while serializing rules from CR ${change.firstChange.diff.rule.id.serialize}") - case Failure(msg, exception, chain) => Result.fromErrorString(msg) - case Full(value) => value + case Left(err) => + Result.fromErrorString( + s"Error while serializing rules from CR ${change.firstChange.diff.rule.id.serialize}: ${err.msg}" + ) + case Right(value) => value } } @@ -774,10 +773,11 @@ object GroupChangeActionJson { case d: GroupDeleteChangeJson => GroupChangeActionJson(ActionChangeJson.delete, d) case d: GroupModifyChangeJson => GroupChangeActionJson(ActionChangeJson.modify, d) }) match { - case Empty => - Result.fromErrorString(s"Error while serializing group from CR ${change.firstChange.diff.group.id.serialize}") - case Failure(msg, exception, chain) => Result.fromErrorString(msg) - case Full(value) => value + case Left(err) => + Result.fromErrorString( + s"Error while serializing group from CR ${change.firstChange.diff.group.id.serialize}: ${err.msg}" + ) + case Right(value) => value } } @@ -888,10 +888,11 @@ object GlobalParameterChangeActionJson { case d: GlobalParameterDeleteChangeJson => GlobalParameterChangeActionJson(ActionChangeJson.delete, d) case d: GlobalParameterModifyChangeJson => GlobalParameterChangeActionJson(ActionChangeJson.modify, d) }) match { - case Empty => - Result.fromErrorString(s"Error while serializing global parameter from CR ${change.firstChange.diff.parameter.name}") - case Failure(msg, exception, chain) => Result.fromErrorString(msg) - case Full(value) => value + case Left(err) => + Result.fromErrorString( + s"Error while serializing global parameter from CR ${change.firstChange.diff.parameter.name}: ${err.msg}" + ) + case Right(value) => value } } diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/api/ChangeRequestApi.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/api/ChangeRequestApi.scala index 6d9d81edf..088ec41e3 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/api/ChangeRequestApi.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/api/ChangeRequestApi.scala @@ -41,7 +41,6 @@ import com.normation.cfclerk.domain.Technique import com.normation.cfclerk.domain.TechniqueId import com.normation.cfclerk.services.TechniqueRepository import com.normation.errors.AccumulateErrors -import com.normation.errors.BoxToIO import com.normation.errors.Inconsistency import com.normation.errors.IOResult import com.normation.errors.OptionToIoResult diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestChangesForm.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestChangesForm.scala index 8cb86d8de..4aef17864 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestChangesForm.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestChangesForm.scala @@ -67,7 +67,7 @@ import com.normation.rudder.web.ChooseTemplate import com.normation.rudder.web.model.* import com.normation.utils.DateFormaterService import com.normation.zio.UnsafeRun -import net.liftweb.common.* +import net.liftweb.common.Loggable import net.liftweb.http.* import net.liftweb.http.js.JE.* import net.liftweb.http.js.JsCmds.* @@ -650,13 +650,11 @@ class ChangeRequestChangesForm( logger.error(err.fullMsg)
    {err.fullMsg}
    } - }) match { - case Full(xml) => - xml - case eb: EmptyBox => - val fail = eb ?~! s"Could not display Rule diffs" - logger.error(fail.messageChain) -
    {fail.messageChain}
    + }).chainError(s"Could not display Rule diffs") match { + case Right(xml) => xml + case Left(err) => + logger.error(err.fullMsg) +
    {err.fullMsg}
    } } }) ++ diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestDetails.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestDetails.scala index 4d8b0a6c9..788a579aa 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestDetails.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestDetails.scala @@ -58,7 +58,11 @@ import com.normation.rudder.web.ChooseTemplate import com.normation.rudder.web.model.* import com.normation.utils.DateFormaterService import com.normation.zio.UnsafeRun -import net.liftweb.common.* +import net.liftweb.common.Box +import net.liftweb.common.EmptyBox +import net.liftweb.common.Failure +import net.liftweb.common.Full +import net.liftweb.common.Loggable import net.liftweb.http.* import net.liftweb.http.js.* import net.liftweb.http.js.JE.* diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestEditForm.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestEditForm.scala index 42c9ab494..264284ce0 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestEditForm.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestEditForm.scala @@ -45,7 +45,7 @@ import com.normation.rudder.users.CurrentUser import com.normation.rudder.web.ChooseTemplate import com.normation.rudder.web.model.* import com.normation.zio.UnsafeRun -import net.liftweb.common.* +import net.liftweb.common.Loggable import net.liftweb.http.* import net.liftweb.http.js.* import net.liftweb.http.js.JsCmds.* diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestManagement.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestManagement.scala index 010017e5b..e3fd95cfe 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestManagement.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/snippet/ChangeRequestManagement.scala @@ -49,7 +49,10 @@ import com.normation.rudder.web.services.JsTableData import com.normation.rudder.web.services.JsTableLine import com.normation.utils.DateFormaterService import com.normation.zio.UnsafeRun -import net.liftweb.common.* +import net.liftweb.common.Box +import net.liftweb.common.EmptyBox +import net.liftweb.common.Full +import net.liftweb.common.Loggable import net.liftweb.http.* import net.liftweb.http.DispatchSnippet import net.liftweb.http.SHtml From 47d7b82c1bcf05a33d70e4947b20215f8e893cae Mon Sep 17 00:00:00 2001 From: Clark Andrianasolo Date: Tue, 6 May 2025 11:47:01 +0200 Subject: [PATCH 52/65] Fixes #26713: Documentation for OIDC opaque/JWT bearer tokens --- auth-backends/README.adoc | 206 +++++++++++++++++++++++++++++++++++++- 1 file changed, 204 insertions(+), 2 deletions(-) diff --git a/auth-backends/README.adoc b/auth-backends/README.adoc index abe01ea45..cc511131c 100644 --- a/auth-backends/README.adoc +++ b/auth-backends/README.adoc @@ -477,7 +477,7 @@ https://openid.net/connect/[OpenID Connect] (OIDC) is a very common SSO protocol These protocols delegate the actual authentication to an identity provider (IdP) that in turns send the relevant authentication information to the client, i.e. to Rudder in our case. These `IdP` can be public providers, like https://google.com[Google], deployed and managed internally in a company, like ForgeRock's open source https://forgerock.github.io/openam-community-edition/[OpenAM], or used as SaaS, like https://okta.com[Okta] - and often, providers do a mix of these things. -Rudder support plain old `OAUTHv2` and `OpentID Connect`. They have several normalized scenario and Rudder supports the most common for a web application server side authentication: https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth[Authentication using Authorization Code Flow]. +Rudder support plain old `OAUTHv2` and `OpenID Connect`. They have several normalized scenario and Rudder supports the most common for a web application server side authentication: https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth[Authentication using Authorization Code Flow]. [NOTE] @@ -811,4 +811,206 @@ It means that the role `node_all` is not recognized. It is because it is not a r In this case you should create a custom role (let's say `access_to_node`) with the permission `node_all` that you will map to your IdP role `role-oidc-a` and modifying the parameter `mapping.entitlements` in the OIDC config file like so: -`rudder.auth.oauth2.provider.okta.roles.mapping.entitlements.role-oidc-a=access_to_node` +`rudder.auth.oauth2.provider.okta.roles.mapping.entitlements.role-oidc-a=access_to_node`. + + +=== OAuth2 tokens : JWT and opaque bearer tokens + +OAuth2 tokens are used to grant access to resources, using authentication tokens, serving the purpose of authenticating machines, automated systems, etc. +There are two types of tokens implemented as authentication backends in Rudder: JWT (JSON Web Tokens) and opaque bearer tokens. + +JWT tokens contain user information and permissions in a JSON format. They can be verified using a signature, allowing for local validation without contacting the authorization server. + +Opaque bearer tokens do not contain user information. They serve as a reference to session or authorization data stored on the server. To validate these tokens, the resource server must contact the authorization server to obtain user details. + +Both are implemented with configuration that is similar to the provisioning of users with OIDC, except that tokens are involved : + +* for JWT, Rudder can locally validate the token's signature, ensuring that the token has not been tampered with and that it is issued by a trusted source. This local validation improves performance and reliability, since it reduces the need for frequent communication with the authorization server (signature keys are fetched from the server, are they are cached, but revoked at some point). +* for opaque bearer tokens, Rudder relies on the authorization server to validate the token. This means that requests with an opaque token requires a call to the authorization server for introspection. While this can introduce some latency, it ensures that the most up-to-date user information is used when resolving token authentication. + +Tenants and roles mapping can be configured for both : + +* for JWT, the token has custom attributes +* for opaque bearer token, the token is bound with an API account in Rudder, which can have ACLs and tenants + +WARNING: In Rudder, you should use only one of both tokens, provided that they are configured correctly (see below for example configurations) + +==== Obtaining and using JWT token in Rudder + +First you will need to obtain a valid JWT token from your IdP, for example from Okta : + +``` +curl --request POST 'https://xxxx.okta.com/oauth2/xxxxaudiencexxxx/v1/token' \ +--header 'Accept: application/json' \ +--header 'Content-Type: application/x-www-form-urlencoded' \ +--header 'Authorization: Basic xxx' \ +--data 'grant_type=client_credentials' +``` + +It will return an access token in the payload that should directly be usable with the Rudder REST API as a `Bearer` token i.e. with the header `Authorization: Bearer the_access_token`. + +Below is an example configuration for handling JWT tokens in Rudder from an external OAuth2 provider `okta`. +You will need to put this configuration in the application properties by adding it to the properties folder, so `/opt/rudder/etc/rudder-web.properties.d/oauth2-api-jwt.properties` by default, and you will need to replace `okta` with your actual provider. + +``` +# Authentication provider id in rudder.auth.provider: +# - OAUTHv2 with JWT: oauth2ApiJwt + +# Configure the list of Identity provider services. Here, you choose +# an identifier for each service as a comma separated list. +# Identifier should be lower case ascii, -, _. For example, if +# your company uses both "Okta" and "Google", you can choose "okta" and +# "google" (how original) identifiers: +rudder.auth.oauth2.jwt.provider.registrations=okta + +# Now, configure Okta related properties. You will need to do +# the same for each provider with an identifier. + +# The identity service provider name as it will be displayed in Rudder +rudder.auth.oauth2.jwt.provider.okta.name=Okta + +# Space separated list of OAUTHv2 "scope" for claims that should be included in the identity +# token once authentication is done. These values should be documented by your IdP documentation. +# Rudder only need to have at least scope which provides the attribute that will be used for +# `userId` (see next property) +rudder.auth.oauth2.jwt.provider.okta.scope=openid email profile groups + +# The single most important property for a JWT token: where the +# IdP public keys that will be used to check the JWT signature are located. +# The audience is part of the uri : +rudder.auth.oauth2.jwt.provider.okta.uri.jwkSet=https://xxxx.okta.com/oauth2/xxx/v1/keys + +# role mapping: OIDC token will provide extended roles to user +rudder.auth.oauth2.jwt.provider.okta.roles.enabled=true +rudder.auth.oauth2.jwt.provider.okta.roles.attribute=customroles +rudder.auth.oauth2.jwt.provider.okta.roles.override=true +rudder.auth.oauth2.jwt.provider.okta.roles.mapping.enforced=true +rudder.auth.oauth2.jwt.provider.okta.roles.mapping.entitlements.rudder_admin=administrator +rudder.auth.oauth2.jwt.provider.okta.roles.mapping.entitlements.rudder_readonly=readonly +rudder.auth.oauth2.jwt.provider.okta.enableProvisioning=true + +# tenants +rudder.auth.oauth2.jwt.provider.okta.tenants.enabled=true +rudder.auth.oauth2.jwt.provider.okta.tenants.attribute=customroles +rudder.auth.oauth2.jwt.provider.okta.tenants.override=true +rudder.auth.oauth2.jwt.provider.okta.tenants.mapping.enforced=true +rudder.auth.oauth2.jwt.provider.okta.tenants.mapping.entitlements.rudder_ta=tenantA +rudder.auth.oauth2.jwt.provider.okta.tenants.mapping.entitlements.rudder_tb=tenantB +rudder.auth.oauth2.jwt.provider.okta.tenants.mapping.entitlements.rudder_admin=* +``` + + +==== Configuration to use opaque bearer tokens + +First you will need to obtain a bearer token from your IdP, for example with Okta using the https://xxxx.okta.com/oauth2/v1/authorize?client_id=...&scope=openid&response_type=token&... + +Then you will need to create an API account in Rudder, without a token (because it is used to identify opaque bearer tokens and to declare authorizations, tenants, etc.). +When creating the API account, The `Account ID` field should have the value of the `cid` (client id) attribute of the opaque bearer token that needs to be identified. + +Once the API account is created and as long as it is not expired, the access token that was obtained above should directly be usable with the Rudder REST API as a `Bearer` token i.e. with the header `Authorization: Bearer the_access_token`. + +Below is an example configuration for handling opaque bearer token tokens in Rudder from an external OAuth2 provider `okta`. +You will need to put this configuration in the application properties by adding it to the properties folder, so `/opt/rudder/etc/rudder-web.properties.d/oauth2-api-opaque.properties` by default, and you will need to replace `okta` with your actual provider. + +``` +# Authentication provider id in rudder.auth.provider: +# - OAUTHv2 with opaque bearer token: oauth2ApiOpaqueToken + +# Configure the list of Identity provider services. Here, you choose +# an identifier for each service as a comma separated list. +# Identifier should be lower case ascii, -, _. For example, if +# your company uses both "Okta" and "Google", you can choose "okta" and +# "google" (how original) identifiers: +rudder.auth.oauth2.opaque.provider.registrations=okta + +# Now, configure Okta related properties. You will need to do +# the same for each provider with an identifier. + +# The identity service provider name as it will be displayed in Rudder +rudder.auth.oauth2.opaque.provider.okta.name=Okta + +# In Oauth2/OIDC, a client (ie, Rudder) is identifier by a pair of credentials: +# - 1/ an id, +# - 2/ a corresponding secret key. +# +# 1/ Identifier of the application you created in your IdP for Rudder. +# In Okta, it will be listed under https://xxxx-admin.okta.com/admin/apps/active +# once you created it with "Create App Integration". If you click on your application, +# it's located in "Client Credential > Client ID". +# +rudder.auth.oauth2.opaque.provider.okta.client.id=xxxx +# +# 2/ The corresponding "client secret", provided by your Identity Provider. +# For Okta, it's available when you click on your application in +# https://xxxx-admin.okta.com/admin/apps/active in "Client Credential > Client Secret" +rudder.auth.oauth2.opaque.provider.okta.client.secret=xxxx + +# +# Opaque bearer tokens must be validated by the IdP on an "introspect" URL. +# This URL is dependant of your local identity provider configuration but +# generally looks like: https://baseurl/v1/introspect, for example for OKTA: +# https://instance-id.okta.com/oauth2/v1/introspect +# +rudder.auth.oauth2.opaque.provider.okta.uri.introspect=https://xxxx.okta.com/oauth2/v1/introspect + +# +# An opaque access token is just used for validating the authentication. A corresponding +# API token must exist in Rudder to define things like token authorization, tenants +# access, etc. The mapping is done thanks to an attribute value of the token. +# By default, and as advised in the OAuth2 standard, we use by default the value +# for attribute `client_id` but some OIDC configuration use something else +# (`sub`, `cid`... ) so this can be changed with that property. +# +# Optional, default "client_id". +# The name of the attribute in the access token that should be used to map to +# Rudder API token ID. +# +rudder.auth.oauth2.opaque.provider.okta.userNameAttributeName=client_id +``` + +==== Common errors during token authentication + +Token authentication can result in errors, simply due to token validity, or due to misconfiguration in the IdP/in Rudder. Here are some common errors and guidelines to help troubleshoot these issues. + +*Token is invalid* + +Such cases may especially happen with JWT, there will be Webapp error logs (in `/var/log/rudder/webapp/webapp.log`) : + +---- +[timestamp] INFO application.authentication - Rudder authentication attempt for principal '[token]' with backend 'oauth2ApiJwt': failure: An error occurred while attempting to decode the Jwt: Malformed token +---- +Ensure that the token is correctly formatted and has not been altered. You can verify the structure of the Base64 encoded JSON locally or by using an https://jwt.io/[online debugger]. + + +*Invalid signature* + +Check that the token's signature matches the expected signature. This often involves verifying the signing key or algorithm used. +Error logs will also be issued by the Webapp : +---- +[timestamp] INFO application.authentication - Rudder authentication attempt for principal '[token]' with backend 'oauth2ApiJwt': failure: An error occurred while attempting to decode the Jwt: Signed JWT rejected: Another algorithm expected, or no matching key(s) found +---- + + +*Token is expired* + +The different types of token have an expiration policy, so a new token will need to be used instead. When authenticating with an expired token, here are some example error logs for JWT and opaque bearer token respectively : + +---- +[timestamp] INFO application.authentication - Rudder authentication attempt for principal '[token]' with backend 'oauth2ApiJwt': failure: An error occurred while attempting to decode the Jwt: Jwt expired at 2025-XX-XXTXX:XX:XXZ +[timestamp] INFO application.authentication - Rudder authentication attempt for principal '[token]' with backend 'oauth2ApiOpaqueToken': failure: Provided token isn't active +---- + +*API account in Rudder does not exist or has expired* + +In the case of opaque bearer tokens, an API account with a specific account ID is needed (it should have the value of the client ID attribute of the generated opaque token). The API account also has an expiration date, that can be modified in Rudder if needed. The error log corresponding to an invalid API account is : +---- +[timestamp] INFO application.authentication - Rudder authentication attempt for principal '[token]' with backend 'oauth2ApiOpaqueToken': failure: An opaque Bearer token was received but No token with ID [accountID] is configured in Rudder +---- + +*Token ID is invalid because due to missing claim* + +In the configuration properties for opaque bearer tokens, there is a `rudder.auth.oauth2.opaque.provider.okta.userNameAttributeName` property that should be an attribute that is defined in the token. Otherwise, an error will be logged and the property will need to be changed (https://jwt.io/[debugging the Base64 token] could help finding the right value) : + +---- +[timestamp] INFO application.authentication - Rudder authentication attempt for principal '[token]' with backend 'oauth2ApiOpaqueToken': failure: An opaque Bearer token was received but it doesn't have a '[userNameAttributeName_value]' claim, so we don't have a token ID and the token is invalid +---- From 6a1c10845de0a78a55e11964db285cfa43e582c9 Mon Sep 17 00:00:00 2001 From: vhayaert Date: Mon, 5 May 2025 18:09:49 +0200 Subject: [PATCH 53/65] Fixes #26862: Migration from Box to ZIO : Refactor ChangeRequestJdbcRepository --- .../ChangeRequestJdbcRepository.scala | 56 ++++++++----------- .../ChangeRequestJdbcRepositoryTest.scala | 7 +-- 2 files changed, 27 insertions(+), 36 deletions(-) diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepository.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepository.scala index 8e2a14024..87c90a70d 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepository.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepository.scala @@ -42,6 +42,7 @@ import cats.syntax.applicative.* import cats.syntax.applicativeError.* import cats.syntax.functor.* import cats.syntax.reducible.* +import com.normation.box.IOToBox import com.normation.errors.* import com.normation.eventlog.EventActor import com.normation.eventlog.ModificationId @@ -62,12 +63,12 @@ import doobie.implicits.* import doobie.postgres.implicits.* import doobie.util.fragments import net.liftweb.common.Box -import net.liftweb.common.EmptyBox import net.liftweb.common.Full import net.liftweb.common.Loggable import org.joda.time.DateTime import scala.xml.Elem import zio.interop.catz.* +import zio.syntax.ToZio trait RoChangeRequestJdbcRepositorySQL { @@ -348,36 +349,27 @@ class ChangeRequestMapper( description: Option[String], content: Elem, modId: Option[String] - ): Box[ChangeRequest] = { - crcUnserialiser.unserialise(content) match { - case Full((directivesMaps, nodesMaps, ruleMaps, paramMaps)) => - directivesMaps match { - case Full(map) => - Full( - ConfigurationChangeRequest( - ChangeRequestId(id), - modId.map(ModificationId.apply), - ChangeRequestInfo( - name.getOrElse(""), - description.getOrElse("") - ), - map, - nodesMaps, - ruleMaps, - paramMaps - ) - ) - - case eb: EmptyBox => - val fail = eb ?~! s"could not deserialize directive change of change request #${id} cause is: ${eb}" - ChangeValidationLogger.error(fail) - fail - } - - case eb: EmptyBox => - val fail = eb ?~! s"Error when trying to get the content of the change request ${id} : ${eb}" - ChangeValidationLogger.error(fail.msg) - fail + ): IOResult[ChangeRequest] = { + crcUnserialiser + .unserialise(content) + .chainError(s"Error when trying to get the content of the change request ${id}") match { + case Right((directivesMaps, nodesMaps, ruleMaps, paramMaps)) => + ConfigurationChangeRequest( + ChangeRequestId(id), + modId.map(ModificationId.apply), + ChangeRequestInfo( + name.getOrElse(""), + description.getOrElse("") + ), + directivesMaps, + nodesMaps, + ruleMaps, + paramMaps + ).succeed + + case Left(err) => + ChangeValidationLogger.error(err.fullMsg) + err.fail } } @@ -402,6 +394,6 @@ class ChangeRequestMapper( } implicit val ChangeRequestRead: Read[Box[ChangeRequest]] = { - Read[CR].map((t: CR) => unserialize(t._1, t._2, t._3, t._4, t._5)) + Read[CR].map((t: CR) => unserialize(t._1, t._2, t._3, t._4, t._5).toBox) } } diff --git a/change-validation/src/test/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepositoryTest.scala b/change-validation/src/test/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepositoryTest.scala index 25707df14..9e61e1d6b 100644 --- a/change-validation/src/test/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepositoryTest.scala +++ b/change-validation/src/test/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepositoryTest.scala @@ -68,7 +68,6 @@ import com.typesafe.config.ConfigValueFactory import doobie.Transactor import doobie.specs2.analysisspec.IOChecker import doobie.syntax.all.* -import net.liftweb.common.Full import org.joda.time.DateTime import org.junit.runner.RunWith import org.specs2.mutable.Specification @@ -133,7 +132,7 @@ class ChangeRequestJdbcRepositoryTest extends Specification with DBCommon with I // same change request to test different xpaths : /changeRequest/directives/directive/@id, "/changeRequest/groups/group/@id", "/changeRequest/rules/rule/@id" // We test change of a directive, nodeGroup and rule using the same change request (sql"insert into ChangeRequest (name, description, creationTime, content, modificationId) values ('a change request', 'a change request description', '2023-01-01T00:00:00', ${sampleChangeRequestContent}, '11111111-1111-1111-1111-111111111111')".update.run *> - sql"insert into Workflow (id, state) values (${changeRequestId}, 'Pending validation')".update.run) + sql"insert into Workflow (id, state) values (${changeRequestId.value}, 'Pending validation')".update.run) .transact(xa) }) match { case Right(_) => () @@ -241,9 +240,9 @@ class ChangeRequestJdbcRepositoryTest extends Specification with DBCommon with I sampleChangeRequestContent } lazy val changeRequestChangesUnserialisation: ChangeRequestChangesUnserialisation = _ => { - Full( + Right( ( - Full(Map(directiveId -> directiveChanges)), + Map(directiveId -> directiveChanges), Map(groupId -> nodeGroupChanges), Map(ruleId -> ruleChanges), Map(globalParamId -> globalParamChanges) From 105189a18da63f9d32e5fd02488307865b25a25b Mon Sep 17 00:00:00 2001 From: vhayaert Date: Fri, 23 May 2025 12:04:58 +0200 Subject: [PATCH 54/65] Fixes #26946: Add the 'Validate All' check box to the WorkflowUsers Elm app --- .../src/main/elm/sources/DataTypes.elm | 1 + .../src/main/elm/sources/Init.elm | 6 +- .../main/elm/sources/SupervisedTargets.elm | 2 +- .../src/main/elm/sources/View.elm | 106 +++++++++++++++--- .../src/main/elm/sources/WorkflowUsers.elm | 4 +- .../template/ChangeValidationManagement.html | 51 ++++++--- 6 files changed, 130 insertions(+), 40 deletions(-) diff --git a/change-validation/src/main/elm/sources/DataTypes.elm b/change-validation/src/main/elm/sources/DataTypes.elm index 916002150..097c76654 100644 --- a/change-validation/src/main/elm/sources/DataTypes.elm +++ b/change-validation/src/main/elm/sources/DataTypes.elm @@ -35,6 +35,7 @@ type alias Model = , leftChecked : List User , hasMoved : List User -- Too track updates , editMod : EditMod + , adminWrite : Bool } getUsernames : UserList -> List Username diff --git a/change-validation/src/main/elm/sources/Init.elm b/change-validation/src/main/elm/sources/Init.elm index 8d228c53e..dd927443f 100644 --- a/change-validation/src/main/elm/sources/Init.elm +++ b/change-validation/src/main/elm/sources/Init.elm @@ -14,6 +14,6 @@ subscriptions _ = Sub.none -initModel : String -> Model -initModel contextPath = - Model contextPath [] [] [] [] [] [] Off +initModel : String -> Bool -> Model +initModel contextPath adminWrite = + Model contextPath [] [] [] [] [] [] Off adminWrite diff --git a/change-validation/src/main/elm/sources/SupervisedTargets.elm b/change-validation/src/main/elm/sources/SupervisedTargets.elm index 2dc7198d9..680d77dc0 100644 --- a/change-validation/src/main/elm/sources/SupervisedTargets.elm +++ b/change-validation/src/main/elm/sources/SupervisedTargets.elm @@ -31,7 +31,7 @@ subscriptions model = ------------------------------ -init : { contextPath : String } -> ( Model, Cmd Msg ) +init : { contextPath : String, adminWrite : Bool } -> ( Model, Cmd Msg ) init flags = let initModel = diff --git a/change-validation/src/main/elm/sources/View.elm b/change-validation/src/main/elm/sources/View.elm index 257081fa0..fc2928d57 100644 --- a/change-validation/src/main/elm/sources/View.elm +++ b/change-validation/src/main/elm/sources/View.elm @@ -3,7 +3,7 @@ module View exposing (..) import ApiCalls exposing (getUsers, saveWorkflow) import DataTypes exposing (ColPos(..), EditMod(..), Model, Msg(..), User, UserList, Username, getUsernames) import Html exposing (..) -import Html.Attributes exposing (attribute, checked, class, disabled, id, style, type_, value) +import Html.Attributes as Attr exposing (attribute, checked, class, disabled, id, style, type_, value) import Html.Events exposing (onCheck, onClick) import List exposing (isEmpty, length, member) import String exposing (fromInt) @@ -291,19 +291,93 @@ displayLeftCol model = view : Model -> Html Msg view model = - div [] - [ - case model.editMod of - On -> - div [class "inner-portlet" ,style "display" "flex", style "justify-content" "center"] - [ - displayLeftCol model - , displayArrows model - , displayRightCol model - ] - Off -> - div [class "inner-portlet" ,style "display" "flex", style "justify-content" "center"] - [ - displayLeftCol model + let + validateAllForm = + if model.adminWrite then + [ Html.br [] [], displayValidateAllForm model ] + + else + [ text "" ] + in + let + workflowUsers = + div [ Attr.class "section-with-doc" ] + [ div [ Attr.class "section-left" ] + [ div + [] + [ case model.editMod of + On -> + div [ class "inner-portlet", style "display" "flex", style "justify-content" "center", id "workflowUsers" ] + [ displayLeftCol model + , displayArrows model + , displayRightCol model + ] + + Off -> + div [ class "inner-portlet", style "display" "flex", style "justify-content" "center" ] + [ displayLeftCol model + ] + ] + ] + , div [ Attr.class "section-right" ] + [ div + [ Attr.class "doc doc-info" ] + [ div [ Attr.class "marker" ] + [ span [ Attr.class "fa fa-info-circle" ] [] ] + , p [] [ text " Any change done by a validated user will be automatically deployed without validation needed by another user. " ] + ] + ] + ] + in + div + [ Attr.id "workflowUsers" ] + (List.append [ workflowUsers ] validateAllForm) + + +displayValidateAllForm : Model -> Html Msg +displayValidateAllForm model = + div + [ Attr.class "section-with-doc" ] + [ div [ Attr.class "section-left" ] + [ form [] + [ ul [] + [ li + [ Attr.class "rudder-form" ] + [ div [ Attr.class "input-group" ] + [ label + [ Attr.class "input-group-addon" + , Attr.for "validationAutoValidatedUser" + ] + [ input + [ Attr.type_ "checkbox" + , Attr.value "Reload" + , Attr.id "validationAutoValidatedUser" + ] + [] + , label + [ Attr.for "validationAutoValidatedUser", Attr.class "label-radio" ] + [ span [ Attr.class "ion ion-checkmark-round" ] [] ] + , span [ Attr.class "ion ion-checkmark-round check-icon" ] [] + ] + , label + [ Attr.class "form-control", Attr.for "validationAutoValidatedUser" ] + [ text " Validate all changes " ] + ] + ] + ] + , input + [ Attr.type_ "submit" + , Attr.value "Save change" + , Attr.id "validationAutoSubmit" + ] + [] + ] + ] + , div [ Attr.class "section-right" ] + [ div [ Attr.class "doc doc-info" ] + [ div [ Attr.class "marker" ] [ span [ Attr.class "fa fa-info-circle" ] [] ] + , p [] [ text " Any change done by a Validated User will be automatically approved no matter the nature of the change. " ] + , p [] [ text " Configuring groups below will hence have no effect on validated users (in the list above), but will apply to non-validated users, who will still need a change request to modify a node from a supervised group. " ] + ] + ] ] - ] \ No newline at end of file diff --git a/change-validation/src/main/elm/sources/WorkflowUsers.elm b/change-validation/src/main/elm/sources/WorkflowUsers.elm index 562844d03..d2f5564c4 100644 --- a/change-validation/src/main/elm/sources/WorkflowUsers.elm +++ b/change-validation/src/main/elm/sources/WorkflowUsers.elm @@ -18,11 +18,11 @@ filterUnvalidatedUsers : UserList -> UserList filterUnvalidatedUsers users = filter (\u -> not u.isValidated) users -mainInit : {contextPath : String} -> ( Model, Cmd Msg ) +mainInit : {contextPath : String, adminWrite: Bool} -> ( Model, Cmd Msg ) mainInit initValues = let m = - initModel initValues.contextPath + initModel initValues.contextPath initValues.adminWrite in ( m, getUsers m ) diff --git a/change-validation/src/main/resources/template/ChangeValidationManagement.html b/change-validation/src/main/resources/template/ChangeValidationManagement.html index a205fc617..c5feb276f 100644 --- a/change-validation/src/main/resources/template/ChangeValidationManagement.html +++ b/change-validation/src/main/resources/template/ChangeValidationManagement.html @@ -180,24 +180,26 @@

    Configure change request triggers

    Configure users with change validation

    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    -

    - Any change done by a validated user will be automatically deployed without validation needed - by another user. -

    +
    +
    + +

    Configure groups with change validations

    @@ -248,12 +252,23 @@

    Configure groups with change validations

    + + + + + + @@ -254,11 +255,11 @@

    Configure groups with change validations

    @@ -268,7 +269,7 @@

    Configure groups with change validations

    var wu = document.getElementById("workflowUsers"); var initValues = { contextPath: contextPath, - adminWrite: adminWrite + hasWriteRights: hasWriteRights }; var app = Elm.SupervisedTargets.init({node: main, flags: initValues}); var app2 = Elm.WorkflowUsers.init({node: wu, flags: initValues}) From e36c835585237044bc2db8a585d4d10bd5a971d2 Mon Sep 17 00:00:00 2001 From: vhayaert Date: Fri, 16 May 2025 18:41:15 +0200 Subject: [PATCH 59/65] Fixes #26904: Migrate the ChangeRequestEditForm snippet from Scala/lift to Elm --- .../elm/sources/ChangeRequestEditForm.elm | 489 ++++++++++++++++++ .../sources/{Notifications.elm => Ports.elm} | 5 +- .../main/elm/sources/SupervisedTargets.elm | 2 +- .../main/elm/sources/WorkflowInformation.elm | 2 +- .../src/main/elm/sources/WorkflowUsers.elm | 2 +- .../resources/template/changeRequest.html | 51 ++ .../changevalidation/ChangeRequestJson.scala | 18 + .../api/ChangeRequestApi.scala | 28 +- 8 files changed, 580 insertions(+), 17 deletions(-) create mode 100644 change-validation/src/main/elm/sources/ChangeRequestEditForm.elm rename change-validation/src/main/elm/sources/{Notifications.elm => Ports.elm} (68%) diff --git a/change-validation/src/main/elm/sources/ChangeRequestEditForm.elm b/change-validation/src/main/elm/sources/ChangeRequestEditForm.elm new file mode 100644 index 000000000..715fc4b1d --- /dev/null +++ b/change-validation/src/main/elm/sources/ChangeRequestEditForm.elm @@ -0,0 +1,489 @@ +module ChangeRequestEditForm exposing (..) + +------------------------------ +-- Init and main -- +------------------------------ + +import Browser +import ErrorMessages exposing (getErrorMessage) +import Html exposing (Html, a, div, form, h2, h3, input, label, p, span, text, textarea) +import Html.Attributes exposing (class, disabled, for, href, id, maxlength, name, readonly, required, style, type_, value) +import Html.Events exposing (onClick, onInput, onSubmit) +import Http exposing (Error, emptyBody, expectJson, header, jsonBody, request) +import Json.Decode exposing (Decoder, andThen, at, fail, field, index, int, map3, map4, map5, string, succeed) +import Json.Encode as Encode +import Ports exposing (errorNotification, readUrl, successNotification) + + +getApiUrl : Model -> String -> String +getApiUrl m url = + m.contextPath ++ "/secure/api/" ++ url + + +changeRequestsPageUrl : Model -> String +changeRequestsPageUrl m = + m.contextPath ++ "/secure/configurationManager/changes/changeRequests" + + +main = + Browser.element + { init = init + , view = view + , update = update + , subscriptions = subscriptions + } + + +init : { contextPath : String, hasWriteRights : Bool } -> ( Model, Cmd Msg ) +init flags = + let + initModel = + Model + flags.contextPath + ChangeRequestIdNotSet + flags.hasWriteRights + NoView + in + ( initModel, Cmd.none ) + + + +------------------------------ +-- MODEL -- +------------------------------ + + +type alias Model = + { contextPath : String + , changeRequest : ChangeRequestDetailsOpt + , hasWriteRights : Bool + , viewState : ViewState + } + + +type Msg + = GetChangeRequestDetails (Result Error ChangeRequestDetails) + | SetChangeRequestDetails (Result Error ChangeRequestDetails) + | GetChangeRequestIdFromUrl String + -- Form input + | FormInputName String + | FormInputDescription String + | FormSubmit + + +type alias ChangeRequestDetails = + { title : String + , state : String + , id : Int + , description : String + } + + +type ChangeRequestDetailsOpt + = Success ChangeRequestDetails + | ChangeRequestIdNotSet + + +type ViewState + = NoView + | ViewError String + | Form { initValues : ChangeRequestDetails, formValues : ChangeRequestDetails } + + + +------------------------------ +-- API -- +------------------------------ + + +getChangeRequestDetails : Model -> Int -> Cmd Msg +getChangeRequestDetails model crId = + let + req = + request + { method = "GET" + , headers = [ header "X-Requested-With" "XMLHttpRequest" ] + , url = getApiUrl model ("changeRequests/" ++ String.fromInt crId) + , body = emptyBody + , expect = expectJson GetChangeRequestDetails decodeChangeRequestDetails + , timeout = Nothing + , tracker = Nothing + } + in + req + + +updateChangeRequestDetails : Model -> ChangeRequestDetails -> Cmd Msg +updateChangeRequestDetails model changeRequestDetails = + let + req = + request + { method = "POST" + , headers = [ header "X-Requested-With" "XMLHttpRequest" ] + , url = getApiUrl model ("changeRequests/" ++ String.fromInt changeRequestDetails.id) + , body = encodeChangeRequestDetails changeRequestDetails |> jsonBody + , expect = expectJson SetChangeRequestDetails decodeChangeRequestDetails + , timeout = Nothing + , tracker = Nothing + } + in + req + + + +------------------------------ +-- ENCODE / DECODE JSON -- +------------------------------ + + +decodeChangeRequestStatus : Decoder String +decodeChangeRequestStatus = + string + |> andThen + (\str -> + case str of + "Open" -> + succeed str + + "Closed" -> + succeed str + + "Pending validation" -> + succeed str + + "Pending deployment" -> + succeed str + + "Cancelled" -> + succeed str + + "Deployed" -> + succeed str + + _ -> + fail "Invalid change request status" + ) + + +decodeChangeRequestDetails : Decoder ChangeRequestDetails +decodeChangeRequestDetails = + at [ "data" ] + (field "changeRequests" + (index 0 + (map4 + ChangeRequestDetails + (field "displayName" string) + (field "status" decodeChangeRequestStatus) + (field "id" int) + (field "description" string) + ) + ) + ) + + +encodeChangeRequestDetails : ChangeRequestDetails -> Encode.Value +encodeChangeRequestDetails changeRequestDetails = + Encode.object + [ ( "description", Encode.string changeRequestDetails.description ) + , ( "name", Encode.string changeRequestDetails.title ) + ] + + + +------------------------------ +-- UPDATE -- +------------------------------ + + +update : Msg -> Model -> ( Model, Cmd Msg ) +update msg model = + case msg of + GetChangeRequestDetails result -> + case result of + Ok changeRequestDetails -> + ( { model | changeRequest = Success changeRequestDetails, viewState = initForm changeRequestDetails }, Cmd.none ) + + Err err -> + let + errMsg = + getErrorMessage err + in + ( { model | viewState = ViewError errMsg } + , errorNotification ("Error while trying to fetch change request details: " ++ errMsg) + ) + + GetChangeRequestIdFromUrl crIdStr -> + case String.toInt crIdStr of + Just crId -> + ( model, getChangeRequestDetails model crId ) + + Nothing -> + let + errMsg = + crIdStr ++ " is not a valid change request id" + in + ( { model | viewState = ViewError errMsg }, errorNotification errMsg ) + + SetChangeRequestDetails result -> + case result of + Ok changeRequestDetails -> + ( { model | changeRequest = Success changeRequestDetails, viewState = initForm changeRequestDetails } + , successNotification "Successfully updated change request details" + ) + + Err err -> + let + errMsg = + getErrorMessage err + in + ( model, errorNotification ("Error while trying to update change request details: " ++ errMsg) ) + + FormInputName newName -> + ( model |> updateForm (setName newName), Cmd.none ) + + FormInputDescription newDescription -> + ( model |> updateForm (setDescription newDescription), Cmd.none ) + + FormSubmit -> + case model.viewState of + Form { initValues, formValues } -> + if canSaveChanges initValues formValues then + ( model, updateChangeRequestDetails model formValues ) + + else + ( model, Cmd.none ) + + _ -> + ( model, Cmd.none ) + + +initForm : ChangeRequestDetails -> ViewState +initForm cr = + Form { initValues = cr, formValues = cr } + + +updateForm : (ViewState -> ViewState) -> Model -> Model +updateForm f model = + { model | viewState = f model.viewState } + + +setDescription : String -> ViewState -> ViewState +setDescription newDescription viewState = + case viewState of + Form ({ formValues } as formState) -> + Form { formState | formValues = { formValues | description = newDescription } } + + _ -> + viewState + + +setName : String -> ViewState -> ViewState +setName newName viewState = + case viewState of + Form formState -> + let + formVal = + formState.formValues + in + Form { formState | formValues = { formVal | title = newName } } + + _ -> + viewState + + +formModified : ChangeRequestDetails -> ChangeRequestDetails -> Bool +formModified initCR formCR = + not (initCR.title == formCR.title) || not (initCR.description == formCR.description) + + +{-| In order to save the changes in the change request form, the form must have been modified +_and_ the change request title mustn't be empty | +-} +canSaveChanges : ChangeRequestDetails -> ChangeRequestDetails -> Bool +canSaveChanges initCR formCR = + formModified initCR formCR && not (String.isEmpty formCR.title) + + + +------------------------------ +-- VIEW -- +------------------------------ + + +view : Model -> Html Msg +view model = + case model.viewState of + Form formState -> + let + canEdit = + model.hasWriteRights + in + div + [ id "change-request-edit-form" ] + [ div + [ class "col-lg-6 col-xs-12 pe-3 ps-3", id "detailsForm" ] + [ form + [ class "needs-validation", id "changeRequestEditForm" ] + [ editCRName formState.formValues.title canEdit + , div + [] + [ label [] [ text "State" ] + , roInputField formState.initValues.state + ] + , div [] + [ label [] [ text "ID" ] + , roInputField (String.fromInt formState.formValues.id) + ] + , editCRDescription formState.formValues.description canEdit + , div + [] + [ saveButton formState.initValues formState.formValues canEdit ] + ] + ] + ] + + NoView -> + errorView model "Change Request Id was not set." + + ViewError errMsg -> + errorView model errMsg + + +roInputField : String -> Html Msg +roInputField content = + input + [ class "form-control col-xs-12" + , disabled True + , readonly True + , value content + ] + [] + + +editCRName : String -> Bool -> Html Msg +editCRName crName writeRights = + let + inputClass = + if crName == "" then + "form-control col-xs-12 is-invalid" + + else + "form-control col-xs-12" + + attributes = + [ name "CRName" + , type_ "text" + , value crName + , id "CRNameLabel" + ] + ++ (if writeRights then + [ class inputClass + , required True + , onInput FormInputName + ] + + else + [ class "form-control col-xs-12" + , disabled True + , readonly True + ] + ) + in + div [ id "CRName" ] + [ div + [ class "row" ] + [ label + [ for "CRNameLabel", class "col-xs-12 form-label" ] + [ span [] [ text "Change request title" ] ] + , div + [ class "needs-validation" ] + [ input attributes [] + , div + [ class "invalid-feedback" ] + [ text "The change request title cannot be empty." ] + ] + ] + ] + + +editCRDescription : String -> Bool -> Html Msg +editCRDescription crDescription writeRights = + let + inputClass = + "form-control col-xs-12" + + attributes = + [ name "CRDescription" + , id "CRDescriptionLabel" + ] + ++ (if writeRights then + [ maxlength 255 + , onInput FormInputDescription + , class inputClass + ] + + else + [ class inputClass + , disabled True + , readonly True + ] + ) + in + div + [ id "CRDescription" ] + [ div + [ class "row" ] + [ label + [ for "CRDescriptionLabel", class "col-xs-12" ] + [ span [ class "fw-normal" ] [ text "Description" ] ] + , div + [ class "col-xs-12" ] + [ textarea attributes [ text crDescription ] + ] + ] + ] + + +saveButton : ChangeRequestDetails -> ChangeRequestDetails -> Bool -> Html Msg +saveButton modelCR formCR writeRights = + if writeRights then + let + attrList = + [ value "Update" + , class "btn btn-default" + , type_ "button" + , onClick FormSubmit + , disabled <| not (canSaveChanges modelCR formCR) + ] + in + div [ id "CRSave" ] + [ input + attrList + [ text "Update" ] + ] + + else + div [ id "CRSave" ] [] + + +errorView : Model -> String -> Html Msg +errorView model errMsg = + div + [ style "padding" "40px" + , style "text-align" "center" + ] + [ h2 [] [ text "Change request id was not found" ] + , h3 [] [ text errMsg ] + , a [ href (changeRequestsPageUrl model) ] [ text "Back to change requests page" ] + ] + + + +------------------------------ +-- SUBSCRIPTIONS +------------------------------ + + +subscriptions : Model -> Sub Msg +subscriptions _ = + readUrl (\id -> GetChangeRequestIdFromUrl id) diff --git a/change-validation/src/main/elm/sources/Notifications.elm b/change-validation/src/main/elm/sources/Ports.elm similarity index 68% rename from change-validation/src/main/elm/sources/Notifications.elm rename to change-validation/src/main/elm/sources/Ports.elm index b3494f80e..4a0cc7a1d 100644 --- a/change-validation/src/main/elm/sources/Notifications.elm +++ b/change-validation/src/main/elm/sources/Ports.elm @@ -1,4 +1,4 @@ -port module Notifications exposing (..) +port module Ports exposing (..) ------------------------------ -- PORTS @@ -9,3 +9,6 @@ port successNotification : String -> Cmd msg port errorNotification : String -> Cmd msg + + +port readUrl : (String -> msg) -> Sub msg diff --git a/change-validation/src/main/elm/sources/SupervisedTargets.elm b/change-validation/src/main/elm/sources/SupervisedTargets.elm index 2dc7198d9..f2ed295b9 100644 --- a/change-validation/src/main/elm/sources/SupervisedTargets.elm +++ b/change-validation/src/main/elm/sources/SupervisedTargets.elm @@ -9,7 +9,7 @@ import Http exposing (..) import Json.Decode as D exposing (Decoder) import Json.Decode.Pipeline exposing (..) import Json.Encode as E -import Notifications exposing (errorNotification, successNotification) +import Ports exposing (errorNotification, successNotification) import Regex import String diff --git a/change-validation/src/main/elm/sources/WorkflowInformation.elm b/change-validation/src/main/elm/sources/WorkflowInformation.elm index 407c57ba5..6c124ca3e 100644 --- a/change-validation/src/main/elm/sources/WorkflowInformation.elm +++ b/change-validation/src/main/elm/sources/WorkflowInformation.elm @@ -6,7 +6,7 @@ import Html exposing (Html, a, i, li, span, text, ul) import Html.Attributes as Attr import Http exposing (Error, emptyBody, expectJson, header, request) import Json.Decode exposing (Decoder, at, bool, field, index, int, list, map2, maybe) -import Notifications exposing (errorNotification) +import Ports exposing (errorNotification) diff --git a/change-validation/src/main/elm/sources/WorkflowUsers.elm b/change-validation/src/main/elm/sources/WorkflowUsers.elm index 562844d03..0c40d0620 100644 --- a/change-validation/src/main/elm/sources/WorkflowUsers.elm +++ b/change-validation/src/main/elm/sources/WorkflowUsers.elm @@ -6,7 +6,7 @@ import DataTypes exposing (ColPos(..), EditMod(..), Model, Msg(..), User, UserLi import ErrorMessages exposing (getErrorMessage) import Init exposing (initModel, subscriptions) import List exposing (filter, member) -import Notifications exposing (errorNotification, successNotification) +import Ports exposing (errorNotification, successNotification) import String import View exposing (view) diff --git a/change-validation/src/main/resources/template/changeRequest.html b/change-validation/src/main/resources/template/changeRequest.html index 353cbf050..934b1c340 100644 --- a/change-validation/src/main/resources/template/changeRequest.html +++ b/change-validation/src/main/resources/template/changeRequest.html @@ -4,6 +4,7 @@ Rudder - Change Validation +
    @@ -16,9 +17,59 @@
    +
    + + + + + + + + + + + + + + + + + +
    diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJson.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJson.scala index c9dd17681..5a6d20e29 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJson.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJson.scala @@ -85,6 +85,7 @@ import com.normation.rudder.domain.properties.PropertyProvider import com.normation.rudder.domain.queries.Query import com.normation.rudder.domain.workflows.ChangeRequest import com.normation.rudder.domain.workflows.ChangeRequestId +import com.normation.rudder.domain.workflows.ChangeRequestInfo import com.normation.rudder.domain.workflows.ConfigurationChangeRequest import com.normation.rudder.domain.workflows.DirectiveChange import com.normation.rudder.domain.workflows.DirectiveChanges @@ -195,6 +196,23 @@ object ChangeRequestJson { } } +final case class ChangeRequestInfoJson( + name: Option[String], + description: Option[String] +) { + + def updateCrInfo(crInfo: ChangeRequestInfo): ChangeRequestInfo = { + crInfo.copy( + name = name.getOrElse(crInfo.name), + description = description.getOrElse(crInfo.description) + ) + } +} + +object ChangeRequestInfoJson { + implicit val decoder: JsonDecoder[ChangeRequestInfoJson] = DeriveJsonDecoder.gen[ChangeRequestInfoJson] +} + /** * Class that represents the number of change requests that are currently in a "pending" status, i.e. * "Pending validation" and "Pending deployment" respectively. diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/api/ChangeRequestApi.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/api/ChangeRequestApi.scala index 088ec41e3..e0ba16f47 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/api/ChangeRequestApi.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/api/ChangeRequestApi.scala @@ -49,6 +49,7 @@ import com.normation.errors.PureResult import com.normation.errors.PureToIoResult import com.normation.errors.Unexpected import com.normation.plugins.changevalidation.ChangeRequestFilter +import com.normation.plugins.changevalidation.ChangeRequestInfoJson import com.normation.plugins.changevalidation.ChangeRequestJson import com.normation.plugins.changevalidation.RoChangeRequestRepository import com.normation.plugins.changevalidation.RoWorkflowRepository @@ -81,7 +82,6 @@ import com.normation.rudder.rest.RudderJsonRequest.* import com.normation.rudder.rest.SortIndex import com.normation.rudder.rest.StartsAtVersion3 import com.normation.rudder.rest.ZeroParam -import com.normation.rudder.rest.data.APIChangeRequestInfo import com.normation.rudder.rest.implicits.* import com.normation.rudder.rest.lift.DefaultParams import com.normation.rudder.rest.lift.LiftApiModule @@ -398,11 +398,15 @@ class ChangeRequestApiImpl( authzToken: AuthzToken ): LiftResponse = { implicit val qc: QueryContext = authzToken.qc - def updateInfo(changeRequest: ChangeRequest, status: WorkflowNodeId, apiInfo: APIChangeRequestInfo)(implicit + def updateInfo(changeRequest: ChangeRequest, status: WorkflowNodeId, apiInfo: ChangeRequestInfoJson)(implicit techniqueByDirective: Map[DirectiveId, Technique] ): IOResult[ChangeRequestJson] = { val newInfo = apiInfo.updateCrInfo(changeRequest.info) - if (changeRequest.info == newInfo) { + + if (newInfo.name == "") { + val message = s"Could not update ChangeRequest ${id} details cause is: Change request name cannot be empty." + Inconsistency(message).fail + } else if (changeRequest.info == newInfo) { val message = s"Could not update ChangeRequest ${id} details cause is: No changes to save." Inconsistency(message).fail } else { @@ -416,9 +420,14 @@ class ChangeRequestApiImpl( } } - withChangeRequestContext(id, params, schema, "update")((changeRequest, status, techniqueByDirective) => - updateInfo(changeRequest, status, extractChangeRequestInfo(req.params))(techniqueByDirective.toMap) - ).toLiftResponseOne(params, schema, Some(id)) + withChangeRequestContext(id, params, schema, "update")((changeRequest, status, techniqueByDirective) => { + for { + json <- req.fromJson[ChangeRequestInfoJson].toIO + update <- updateInfo(changeRequest, status, json)(techniqueByDirective.toMap) + } yield { + update + } + }).toLiftResponseOne(params, schema, Some(id)) } } @@ -514,13 +523,6 @@ class ChangeRequestApiImpl( }) } - private def extractChangeRequestInfo(params: Map[String, List[String]]): APIChangeRequestInfo = { - APIChangeRequestInfo( - params.get("name").flatMap(_.headOption), - params.get("description").flatMap(_.headOption) - ) - } - private[this] def extractFilters(params: Map[String, List[String]]): PureResult[ChangeRequestFilter] = { import ChangeRequestFilter.* for { From 47cd7d3150eaa36b8701ef6bebafb849a4c77cc5 Mon Sep 17 00:00:00 2001 From: vhayaert Date: Mon, 26 May 2025 12:35:37 +0200 Subject: [PATCH 60/65] Fixes #26953: ChangeRequestJdbcRepository ignores error when change request unserialization fails --- .../ChangeRequestJdbcRepository.scala | 214 ++++++++++++------ 1 file changed, 139 insertions(+), 75 deletions(-) diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepository.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepository.scala index 87c90a70d..a5b8ae6eb 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepository.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/ChangeRequestJdbcRepository.scala @@ -37,24 +37,31 @@ package com.normation.plugins.changevalidation +import cats.Show import cats.data.NonEmptyList +import cats.implicits.toBifunctorOps import cats.syntax.applicative.* import cats.syntax.applicativeError.* import cats.syntax.functor.* import cats.syntax.reducible.* -import com.normation.box.IOToBox import com.normation.errors.* import com.normation.eventlog.EventActor import com.normation.eventlog.ModificationId import com.normation.rudder.db.Doobie import com.normation.rudder.db.Doobie.* import com.normation.rudder.domain.nodes.NodeGroupId +import com.normation.rudder.domain.policies.DirectiveId import com.normation.rudder.domain.policies.DirectiveUid +import com.normation.rudder.domain.policies.RuleId import com.normation.rudder.domain.policies.RuleUid import com.normation.rudder.domain.workflows.ChangeRequest import com.normation.rudder.domain.workflows.ChangeRequestId import com.normation.rudder.domain.workflows.ChangeRequestInfo import com.normation.rudder.domain.workflows.ConfigurationChangeRequest +import com.normation.rudder.domain.workflows.DirectiveChanges +import com.normation.rudder.domain.workflows.GlobalParameterChanges +import com.normation.rudder.domain.workflows.NodeGroupChanges +import com.normation.rudder.domain.workflows.RuleChanges import com.normation.rudder.domain.workflows.WorkflowNodeId import com.normation.rudder.services.marshalling.ChangeRequestChangesSerialisation import com.normation.rudder.services.marshalling.ChangeRequestChangesUnserialisation @@ -68,7 +75,18 @@ import net.liftweb.common.Loggable import org.joda.time.DateTime import scala.xml.Elem import zio.interop.catz.* -import zio.syntax.ToZio +import zio.syntax.* + +/** + * Change request from the database does not have a trivial mapping to the `ChangeRequest` structure : + * there is the XML content that needs to be validated, and in some cases (some APIs) we decide to + * return the valid ones and ignore invalid ones. + * + * So, the type here is used to create a Read that does not fail but contains the failure. + */ +sealed trait DbChangeRequest +case class ChangeRequestWithSuccessXml(cr: ChangeRequest) extends DbChangeRequest +case class ChangeRequestWithInvalidXml(error: RudderError) extends DbChangeRequest trait RoChangeRequestJdbcRepositorySQL { @@ -83,43 +101,72 @@ trait RoChangeRequestJdbcRepositorySQL { import changeRequestMapper.* - implicit val readCRWithState: Read[Box[(ChangeRequest, WorkflowNodeId)]] = { - Read[(Box[ChangeRequest], WorkflowNodeId)].map { case (cr, state) => cr.map((_, state)) } + implicit val ChangeRequestReadOpt: Read[DbChangeRequest] = { + Read[CR].map { + case (id, name, description, content, modId) => + crcUnserialiser + .unserialise(content) + .chainError(s"Error when trying to get the content of the change request ${id}") match { + case Right((directivesMaps, nodesMaps, ruleMaps, paramMaps)) => + ChangeRequestWithSuccessXml( + ConfigurationChangeRequest( + ChangeRequestId(id), + modId.map(ModificationId.apply), + ChangeRequestInfo( + name.getOrElse(""), + description.getOrElse("") + ), + directivesMaps, + nodesMaps, + ruleMaps, + paramMaps + ) + ) + + case Left(err) => + ChangeRequestWithInvalidXml(err) + } + } } - def getAllSQL: Query0[Box[ChangeRequest]] = - sql"SELECT id, name, description, content, modificationId FROM ChangeRequest".query[Box[ChangeRequest]] + def getAllSQL: Query0[DbChangeRequest] = + sql"SELECT id, name, description, content, modificationId FROM ChangeRequest".query[DbChangeRequest] + + def getSQL(changeRequestId: ChangeRequestId): Query0[ChangeRequest] = { + sql"SELECT id, name, description, content, modificationId FROM ChangeRequest where id = ${changeRequestId}" + .query[ChangeRequest] + } - def getSQL(changeRequestId: ChangeRequestId): Query0[Box[ChangeRequest]] = { + def getRawCRSQL(changeRequestId: ChangeRequestId): Query0[DbChangeRequest] = { sql"SELECT id, name, description, content, modificationId FROM ChangeRequest where id = ${changeRequestId}" - .query[Box[ChangeRequest]] + .query[DbChangeRequest] } - def getByContributorSQL(actor: EventActor): Query0[Box[ChangeRequest]] = { + def getByContributorSQL(actor: EventActor): Query0[ChangeRequest] = { val actorName = Array(actor.name) sql"SELECT id, name, description, content, modificationId FROM ChangeRequest where cast( xpath('//firstChange/change/actor/text()',content) as character varying[]) = ${actorName}" - .query[Box[ChangeRequest]] + .query[ChangeRequest] } def getChangeRequestsByXpathContentSQL( xpath: Fragment, shouldEquals: String, onlyPending: Boolean - ): Query0[Box[ChangeRequest]] = { + ): Query0[ChangeRequest] = { val param = Array(shouldEquals) if (onlyPending) { sql"""SELECT CR.id, name, description, content, modificationId FROM changeRequest CR LEFT JOIN workflow W on CR.id = W.id where cast( xpath(${xpath}, content) as character varying[]) = ${param} and state like 'Pending%'""" - .query[Box[ChangeRequest]] + .query[ChangeRequest] } else { sql"""SELECT id, name, description, content, modificationId FROM ChangeRequest where cast( xpath(${xpath}, content) as character varying[]) = ${param}""" - .query[Box[ChangeRequest]] + .query[ChangeRequest] } } def getByFiltersSQL( statuses: Option[NonEmptyList[WorkflowNodeId]], xpathWithValue: Option[(Fragment, String)] // (xpath, value) - ): Query0[Box[(ChangeRequest, WorkflowNodeId)]] = { + ): Query0[(ChangeRequest, WorkflowNodeId)] = { (fr"SELECT CR.id, CR.name, CR.description, CR.content, CR.modificationId, W.state FROM ChangeRequest CR LEFT JOIN workflow W on CR.id = W.id" ++ fragments.whereAndOpt( statuses.map(fragments.in(fr"state", _)), @@ -128,7 +175,7 @@ trait RoChangeRequestJdbcRepositorySQL { val param = Array(value) fr"cast( xpath(${xpath}, content) as character varying[]) = ${param}" } - )).query[Box[(ChangeRequest, WorkflowNodeId)]] + )).query[(ChangeRequest, WorkflowNodeId)] } } @@ -160,25 +207,39 @@ class RoChangeRequestJdbcRepository( import doobie.* // utility method which correctly transform Doobie types towards Box[Vector[ChangeRequest]] - private[this] def execQuery(errMsg: String, q: Query0[Box[ChangeRequest]]): IOResult[Vector[ChangeRequest]] = { - transactIOResult(errMsg)(xa => { - q.to[Vector] - .map( - // we are just ignoring change request with unserialisation - // error. Does not seem the best. - _.flatten.toVector - ) - .transact(xa) - }) + private[this] def execQuery(errMsg: String, q: Query0[ChangeRequest]): IOResult[Vector[ChangeRequest]] = { + transactIOResult(errMsg)(xa => { q.to[Vector].transact(xa) }) } override def getAll(): IOResult[Vector[ChangeRequest]] = { - execQuery("Could not get all change requests in database", getAllSQL) + transactIOResult("errMsg")(xa => { + getAllSQL + .to[List] + .transact(xa) + .flatMap(vector => { + val (errors, success) = vector.partitionMap { + case ChangeRequestWithInvalidXml(err) => Left(err) + case ChangeRequestWithSuccessXml(suc) => Right(suc) + } + + NonEmptyList + .fromList(errors) + .succeed + .tapSome { + case Some(errs) => + ChangeValidationLoggerPure + .warn( + Chained("There are some errors getting all change requests", Accumulated(errs)).fullMsg + ) + } + .as(success.toVector) + }) + }) } override def get(changeRequestId: ChangeRequestId): IOResult[Option[ChangeRequest]] = { transactIOResult(s"Could not get change request with id ${changeRequestId} in database")(xa => - getSQL(changeRequestId).option.map(_.flatMap(_.toOption)).transact(xa) + getSQL(changeRequestId).option.transact(xa) ) } @@ -236,7 +297,7 @@ class RoChangeRequestJdbcRepository( getByFiltersSQL(statuses.map(_.toNonEmptyList), by.map(getXPathWithValue)) } - transactIOResult(errorMsg)(filteredQuery.to[Vector].map(_.flatten).transact(_)) + transactIOResult(errorMsg)(filteredQuery.to[Vector].transact(_)) } } @@ -274,17 +335,16 @@ class WoChangeRequestJdbcRepository( val (name, desc, xml, modId) = getAtom(changeRequest) - for { - id <- transactIOResult(s"Could not create change request with id ${changeRequest.id} in database")(xa => - createChangeRequestSQL(name, desc, xml, modId).withUniqueGeneratedKeys[Int]("id").transact(xa) - ) - cr <- roRepo - .get(ChangeRequestId(id)) - .notOptional(s"The newly saved change request with id ${id} was not found back in database") - .tapError(err => ChangeValidationLoggerPure.error(err.fullMsg)) - } yield { - cr - } + transactIOResult[Option[ChangeRequest]](s"Could not create change request with id ${changeRequest.id} in database")(xa => { + (for { + id <- createChangeRequestSQL(name, desc, xml, modId).withUniqueGeneratedKeys[Int]("id") + cr <- getSQL(ChangeRequestId(id)).option + } yield { + cr + }).transact(xa) + }) + .notOptional(s"The new change request cannot be saved in database") + .tapError(err => ChangeValidationLoggerPure.error(err.fullMsg)) } /** @@ -309,7 +369,7 @@ class WoChangeRequestJdbcRepository( // no transaction between steps, because we don't actually use anything in the existing change request val process = { for { - exists <- getSQL(changeRequest.id).option + exists <- getRawCRSQL(changeRequest.id).option _ <- exists match { case None => val msg = @@ -340,39 +400,6 @@ class ChangeRequestMapper( // id, name, description, content, modificationId type CR = (Int, Option[String], Option[String], Elem, Option[String]) - // unserialize the XML. - // If it fails, produce a failure - // directives map is stored in a IOResult because an Exception could be launched - def unserialize( - id: Int, - name: Option[String], - description: Option[String], - content: Elem, - modId: Option[String] - ): IOResult[ChangeRequest] = { - crcUnserialiser - .unserialise(content) - .chainError(s"Error when trying to get the content of the change request ${id}") match { - case Right((directivesMaps, nodesMaps, ruleMaps, paramMaps)) => - ConfigurationChangeRequest( - ChangeRequestId(id), - modId.map(ModificationId.apply), - ChangeRequestInfo( - name.getOrElse(""), - description.getOrElse("") - ), - directivesMaps, - nodesMaps, - ruleMaps, - paramMaps - ).succeed - - case Left(err) => - ChangeValidationLogger.error(err.fullMsg) - err.fail - } - } - def serialize(optCR: Box[ChangeRequest]): CR = { optCR match { case Full(cr) => @@ -393,7 +420,44 @@ class ChangeRequestMapper( } } - implicit val ChangeRequestRead: Read[Box[ChangeRequest]] = { - Read[CR].map((t: CR) => unserialize(t._1, t._2, t._3, t._4, t._5).toBox) + type ElemXml = ( + Map[DirectiveId, DirectiveChanges], + Map[NodeGroupId, NodeGroupChanges], + Map[RuleId, RuleChanges], + Map[String, GlobalParameterChanges] + ) + + implicit val ElemShow: Show[Elem] = { + Show.show(e => e.toString()) + } + + implicit val ElemXmlGet: Get[ElemXml] = XmlMeta.get.temap(content => { + crcUnserialiser + .unserialise(content) + .leftMap(err => Chained("Could not get content column from change request ", err).fullMsg) + }) + + implicit val ChangeRequestShow: Show[CR] = { + Show.show(cr => s"( id : ${cr._1}, name : ${cr._2}, description : ${cr._3}, content : ${cr._4}, modificationId : ${cr._5} ") + } + + implicit val ChangeRequestRead: Read[ChangeRequest] = { + + Read[(Int, Option[String], Option[String], ElemXml, Option[String])].map { + + case (id, name, description, (directivesMaps, nodesMaps, ruleMaps, paramMaps), modId) => + ConfigurationChangeRequest( + ChangeRequestId(id), + modId.map(ModificationId.apply), + ChangeRequestInfo( + name.getOrElse(""), + description.getOrElse("") + ), + directivesMaps, + nodesMaps, + ruleMaps, + paramMaps + ) + } } } From 991f18e62ca4a20801fe3b3d4937b5d7dab16062 Mon Sep 17 00:00:00 2001 From: vhayaert Date: Mon, 2 Jun 2025 10:39:33 +0200 Subject: [PATCH 61/65] Fixes #27007: API tests no longer pass because of ChangeRequestApi changes --- .../api_changerequest.yml | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/change-validation/src/test/resources/changevalidation_api/api_changerequest.yml b/change-validation/src/test/resources/changevalidation_api/api_changerequest.yml index e964b3639..8d342bfd5 100644 --- a/change-validation/src/test/resources/changevalidation_api/api_changerequest.yml +++ b/change-validation/src/test/resources/changevalidation_api/api_changerequest.yml @@ -3610,10 +3610,12 @@ description: Update information about given change request method: POST url: /api/latest/changeRequests/4 headers: - - "Content-Type: application/x-www-form-urlencoded" -params: - name: "my group new name" - description: "My group new description" + - "Content-Type: application/json" +body: >- + { + "name": "my group new name", + "description": "My group new description" + } response: code: 200 content: >- @@ -3728,6 +3730,10 @@ response: description: Update information about given change request when no parameter is sent method: POST url: /api/latest/changeRequests/4 +headers: + - "Content-Type: application/json" +body: >- + {} response: code: 500 content: >- @@ -3742,10 +3748,12 @@ description: Update information about given change request without changing any method: POST url: /api/latest/changeRequests/11 headers: - - "Content-Type: application/x-www-form-urlencoded" -params: - name: "second cr global param" - description: "My global param second change" + - "Content-Type: application/json" +body: >- + { + "name": "second cr global param", + "description": "My global param second change" + } response: code: 500 content: >- From a7d936c4e6a721163457b88d2b66ba9713573886 Mon Sep 17 00:00:00 2001 From: Clark Andrianasolo Date: Tue, 3 Jun 2025 15:33:52 +0200 Subject: [PATCH 62/65] Fixes #27029: Change request count Elm WorkflowInformation app needs a loader --- .../main/elm/sources/WorkflowInformation.elm | 66 +++++++------- .../changevalidation/TopBarExtension.scala | 25 ++++- .../comet/WorkflowInformation.scala | 91 ++++++++----------- 3 files changed, 97 insertions(+), 85 deletions(-) diff --git a/change-validation/src/main/elm/sources/WorkflowInformation.elm b/change-validation/src/main/elm/sources/WorkflowInformation.elm index 6c124ca3e..12f8aa632 100644 --- a/change-validation/src/main/elm/sources/WorkflowInformation.elm +++ b/change-validation/src/main/elm/sources/WorkflowInformation.elm @@ -222,32 +222,27 @@ update msg model = view : Model -> Html Msg view model = - case model.pendingCount of - PendingCountWithTotal pc -> - li - [ Attr.class "nav-item dropdown notifications-menu" - , Attr.id "workflow-app" - ] - [ a - [ Attr.href "#" - , Attr.class "dropdown-toggle" - , Attr.attribute "data-bs-toggle" "dropdown" - , Attr.attribute "role" "button" - , Attr.attribute "aria-expanded" "false" - ] - [ span [] - [ text "CR" ] - , viewDropdownToggle pc.totalCount - ] - , ul - [ Attr.class "dropdown-menu" - , Attr.attribute "role" "menu" - ] - [ li [] [ viewDropDownMenu model ] ] - ] + let + viewDropdown = + case model.pendingCount of + NotSet -> + viewDropdownToggle True "-" + + PendingCountWithTotal pc -> + viewDropdownToggle False ( String.fromInt pc.totalCount ) + in + li + [ Attr.class "nav-item dropdown notifications-menu" + , Attr.id "workflow-app" + ] + [ viewDropdown + , ul + [ Attr.class "dropdown-menu" + , Attr.attribute "role" "menu" + ] + [ li [] [ viewDropDownMenu model ] ] + ] - NotSet -> - text "" viewDropDownMenu : Model -> Html Msg @@ -263,13 +258,22 @@ viewDropDownMenu model = ] -viewDropdownToggle : Int -> Html Msg -viewDropdownToggle totalCount = - span - [ Attr.id "number" - , Attr.class "badge rudder-badge" +viewDropdownToggle : Bool -> String -> Html Msg +viewDropdownToggle isLoading displayedCount = + a + [ Attr.href "#" + , Attr.class ( "dropdown-toggle " ++ if isLoading then "placeholder-glow" else "" ) + , Attr.attribute "data-bs-toggle" "dropdown" + , Attr.attribute "role" "button" + , Attr.attribute "aria-expanded" "false" + ] + [ span [] [ text "CR" ] + , span + [ Attr.id "number" + , Attr.class ( "badge rudder-badge " ++ if isLoading then "placeholder" else "" ) + ] + [ Html.text displayedCount ] ] - [ Html.text (String.fromInt totalCount) ] displayPendingCount : Maybe Int -> String -> String -> String -> Html Msg diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/TopBarExtension.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/TopBarExtension.scala index b7f467e5c..52536f96d 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/TopBarExtension.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/TopBarExtension.scala @@ -2,12 +2,17 @@ package com.normation.plugins.changevalidation import com.normation.plugins.PluginExtensionPoint import com.normation.plugins.PluginStatus +import com.normation.rudder.AuthorizationType +import com.normation.rudder.users.CurrentUser import com.normation.rudder.web.snippet.CommonLayout import net.liftweb.common.Loggable import net.liftweb.util.Helpers.* import scala.reflect.ClassTag import scala.xml.NodeSeq +/** + * Load the change-validation scripts, css and async comet actor : to display it in the top bar + */ class TopBarExtension(val status: PluginStatus)(implicit val ttag: ClassTag[CommonLayout]) extends PluginExtensionPoint[CommonLayout] with Loggable { @@ -15,10 +20,24 @@ class TopBarExtension(val status: PluginStatus)(implicit val ttag: ClassTag[Comm "display" -> render _ ) + /** + * User must have either or both of the Validator.Read and Deployer.Read authorizations + * in order to display this. + */ def render(xml: NodeSeq) = { - ( - "#rudder-navbar -*" #>
  • - ).apply(xml) + val isValidator = CurrentUser.checkRights(AuthorizationType.Validator.Read) + val isDeployer = CurrentUser.checkRights(AuthorizationType.Deployer.Read) + if (isValidator || isDeployer) { + ( + "#rudder-navbar -*" #> + + + + + & "#rudder-navbar -*" #> +
  • + ).apply(xml) + } else xml } } diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/comet/WorkflowInformation.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/comet/WorkflowInformation.scala index 71f215135..7c7f282de 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/comet/WorkflowInformation.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/comet/WorkflowInformation.scala @@ -38,32 +38,29 @@ package com.normation.plugins.changevalidation.comet import bootstrap.liftweb.RudderConfig -import com.normation.rudder.AuthorizationType import com.normation.rudder.batch.AsyncWorkflowInfo import com.normation.rudder.services.workflows.WorkflowUpdate -import com.normation.rudder.users.CurrentUser import com.normation.zio.UnsafeRun -import net.liftweb.common.Empty -import net.liftweb.common.Full -import net.liftweb.common.Loggable +import net.liftweb.common.* import net.liftweb.http.* +import net.liftweb.http.js.JsCmds.Run import scala.xml.* +/** + * Actor to dynamically update the count of change requests once workflows are updated. + * + * It loads the corresponding WorkflowInformation Elm app, which can also be + * dynamically loaded depending on feature toggle. + */ class WorkflowInformation extends CometActor with CometListener with Loggable { private[this] val asyncWorkflow = RudderConfig.asyncWorkflowInfo - private[this] val isValidator = CurrentUser.checkRights(AuthorizationType.Validator.Read) - private[this] val isDeployer = CurrentUser.checkRights(AuthorizationType.Deployer.Read) - private[this] var workflowEnabledPrev = getWorkflowEnabled() private[this] var shouldLoadScript = workflowEnabledPrev - /** A user must have either or both of the Validator.Read and Deployer.Read authorizations - * in order to display the pending change requests menu. */ - private[this] def hasRights() = isValidator || isDeployer private[this] def getWorkflowEnabled() = { - if (hasRights()) RudderConfig.configService.rudder_workflow_enabled().orElseSucceed(false).runNow else false + RudderConfig.configService.rudder_workflow_enabled().orElseSucceed(false).runNow } override def registerWith: AsyncWorkflowInfo = asyncWorkflow @@ -71,10 +68,8 @@ class WorkflowInformation extends CometActor with CometListener with Loggable { override val defaultHtml = NodeSeq.Empty def render: RenderOut = { - - if (!hasRights()) new RenderOut(NodeSeq.Empty) - - /* xml is a menu entry which looks like : + /* + A menu entry created by the WorkflowInformation Elm app, when mounted on #workflow-app, it looks like : } - - RenderOut(Full(xml), Full(fixedLayout), Empty, Empty, ignoreHtmlOnJs = false) + loadScript() + new RenderOut(xml) } override def lowPriority = { case WorkflowUpdate => - if (!hasRights()) () - val workflowEnabled = getWorkflowEnabled() // The script should be loaded if the workflow_enabled setting has been enabled since the last render @@ -139,6 +108,26 @@ class WorkflowInformation extends CometActor with CometListener with Loggable { workflowEnabledPrev = workflowEnabled - reRender() + loadScript() + } + + private def loadScript(): Unit = { + if (shouldLoadScript) { + partialUpdate(Run(""" + $(document).ready(function(){ + const node = document.getElementById("workflow-app"); + if (!node) return; + const initValues = { + contextPath : contextPath + }; + const app = Elm.WorkflowInformation.init({node: node, flags: initValues}); + + app.ports.errorNotification.subscribe((errMsg) => { + createErrorNotification(errMsg) + }) + }); + """)) + } } + } From 42a18f38aaa5d07904f80688e16ed5e1445b08b2 Mon Sep 17 00:00:00 2001 From: Clark Andrianasolo Date: Tue, 3 Jun 2025 15:52:39 +0200 Subject: [PATCH 63/65] Fixes #26951: Plugins need CSP to be strict in Rudder but disabled in plugin pages --- .../UserInformationExtension.scala | 2 +- .../template/brandingManagement.html | 2 +- .../template/ChangeValidationManagement.html | 6 ++--- .../resources/template/changeRequest.html | 12 ++++----- .../comet/WorkflowInformation.scala | 4 +-- .../snippet/ChangeRequestChangesForm.scala | 25 ++++++++++++------- .../snippet/ChangeValidationSettings.scala | 3 ++- .../template/dataSourceManagement.html | 6 ++--- 8 files changed, 34 insertions(+), 26 deletions(-) diff --git a/api-authorizations/src/main/scala/com/normation/plugins/apiauthorizations/UserInformationExtension.scala b/api-authorizations/src/main/scala/com/normation/plugins/apiauthorizations/UserInformationExtension.scala index 2c151f82b..31e05a638 100644 --- a/api-authorizations/src/main/scala/com/normation/plugins/apiauthorizations/UserInformationExtension.scala +++ b/api-authorizations/src/main/scala/com/normation/plugins/apiauthorizations/UserInformationExtension.scala @@ -85,7 +85,7 @@ class UserInformationExtension(
    - - - - - - - - - - - - +
    @@ -181,143 +179,35 @@

    Configure change request triggers

    Configure users with change validation

    -
    - -
    - - -
    -

    Configure groups with change validations

    -
    -
    -
    -
    -
    +
    + + - - - - - -
    -
    -
    -
    - -
    -

    - Change validation are enable for any change that would impact a node belonging to one - of the chosen groups below. - Be careful: a change on one another group -

    -

    - The supervised changes are: -

      -
    • any change in a global parameter, as these changes can have side effects spreading - technique code,
    • -
    • any modification in one of the supervised groups,
    • -
    • any change in a rule which targets a node which belong to a group marked as supervised, -
    • -
    • any change in a directive used in one of the previous rules.
    • -
    -

    -

    - Changes in techniques are not subjected to change validation, nor are changes resulting from - an archive import. -

    -
    -
    -
    -
    + + + +
    diff --git a/change-validation/src/main/style/change-validation.css b/change-validation/src/main/style/change-validation.css index d2b095b31..72632e8c9 100644 --- a/change-validation/src/main/style/change-validation.css +++ b/change-validation/src/main/style/change-validation.css @@ -380,7 +380,7 @@ ul.clipboard-list > li:last-child{ border-radius: 3px; } -#supervised-targets-app .node-check .ion{ +#supervised-targets-app .node-check .fa{ font-size: 12px; top: -10px; position: relative; @@ -395,7 +395,7 @@ ul.clipboard-list > li:last-child{ display: none; } -#supervised-targets-app .node-check input[type=checkbox]:checked + .ion { +#supervised-targets-app .node-check input[type=checkbox]:checked + .fa { visibility: visible; animation: opacity-1 .1s linear forwards; } From b2495327d830d14572e6653ed186f180d1c7237c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Membr=C3=A9?= Date: Wed, 4 Jun 2025 15:27:58 +0200 Subject: [PATCH 65/65] Prepare next nightly branch of plugin --- main-build.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main-build.conf b/main-build.conf index 5f2de0036..3ff046c23 100644 --- a/main-build.conf +++ b/main-build.conf @@ -14,6 +14,6 @@ # Version of Rudder used to build the plugin. # It defined the API/ABI used and it is important for binary compatibility branch-type=next -rudder-version=8.3.2 +rudder-version=8.3.3 common-version=2.1.1 private-version=2.1.0