diff --git a/api/v1/mapping/src/commonMain/kotlin/ApiMappings.kt b/api/v1/mapping/src/commonMain/kotlin/ApiMappings.kt index dbceb671c6..c84889125b 100644 --- a/api/v1/mapping/src/commonMain/kotlin/ApiMappings.kt +++ b/api/v1/mapping/src/commonMain/kotlin/ApiMappings.kt @@ -113,15 +113,17 @@ import org.eclipse.apoapsis.ortserver.model.OrtRun import org.eclipse.apoapsis.ortserver.model.OrtRunFilters import org.eclipse.apoapsis.ortserver.model.OrtRunStatus import org.eclipse.apoapsis.ortserver.model.OrtRunSummary -import org.eclipse.apoapsis.ortserver.model.PluginConfig import org.eclipse.apoapsis.ortserver.model.Product import org.eclipse.apoapsis.ortserver.model.ProviderPluginConfiguration import org.eclipse.apoapsis.ortserver.model.ReporterJob import org.eclipse.apoapsis.ortserver.model.ReporterJobConfiguration import org.eclipse.apoapsis.ortserver.model.Repository import org.eclipse.apoapsis.ortserver.model.RepositoryType +import org.eclipse.apoapsis.ortserver.model.ResolvablePluginConfig +import org.eclipse.apoapsis.ortserver.model.ResolvableSecret import org.eclipse.apoapsis.ortserver.model.ScannerJob import org.eclipse.apoapsis.ortserver.model.ScannerJobConfiguration +import org.eclipse.apoapsis.ortserver.model.SecretSource import org.eclipse.apoapsis.ortserver.model.Severity import org.eclipse.apoapsis.ortserver.model.SourceCodeOrigin import org.eclipse.apoapsis.ortserver.model.SubmoduleFetchStrategy @@ -692,9 +694,15 @@ fun PackageManagerConfiguration.mapToApi() = fun ApiPackageManagerConfiguration.mapToModel() = PackageManagerConfiguration(mustRunAfter = mustRunAfter, options = options) -fun PluginConfig.mapToApi() = ApiPluginConfig(options = options, secrets = secrets) +fun ResolvablePluginConfig.mapToApi() = ApiPluginConfig( + options = options, + secrets = secrets.mapValues { it.value.name } +) -fun ApiPluginConfig.mapToModel() = PluginConfig(options = options, secrets = secrets) +fun ApiPluginConfig.mapToModel() = ResolvablePluginConfig( + options = options, + secrets = secrets.mapValues { ResolvableSecret(it.value, SecretSource.ADMIN) } +) fun ProviderPluginConfiguration.mapToApi() = ApiProviderPluginConfiguration( diff --git a/components/plugin-manager/backend/src/main/kotlin/PluginTemplateService.kt b/components/plugin-manager/backend/src/main/kotlin/PluginTemplateService.kt index b762b096a4..f588041b74 100644 --- a/components/plugin-manager/backend/src/main/kotlin/PluginTemplateService.kt +++ b/components/plugin-manager/backend/src/main/kotlin/PluginTemplateService.kt @@ -32,7 +32,7 @@ import com.github.michaelbull.result.toResultOr import org.eclipse.apoapsis.ortserver.components.pluginmanager.queries.GetPluginTemplateForOrganizationQuery import org.eclipse.apoapsis.ortserver.components.pluginmanager.queries.GetPluginTemplateQuery import org.eclipse.apoapsis.ortserver.components.pluginmanager.queries.GetPluginTemplatesQuery -import org.eclipse.apoapsis.ortserver.model.PluginConfig +import org.eclipse.apoapsis.ortserver.model.ResolvablePluginConfig import org.eclipse.apoapsis.ortserver.model.repositories.OrganizationRepository import org.eclipse.apoapsis.ortserver.model.repositories.RepositoryRepository @@ -349,7 +349,7 @@ class PluginTemplateService( * [organization][organizationId]. */ fun validatePluginConfigs( - pluginConfigs: Map>, + pluginConfigs: Map>, organizationId: Long ): PluginConfigValidationResult { validateOrganizationExists(organizationId).onFailure { error -> diff --git a/components/secrets/backend/src/main/kotlin/SecretService.kt b/components/secrets/backend/src/main/kotlin/SecretService.kt index 5ad620868e..1891c06d2b 100644 --- a/components/secrets/backend/src/main/kotlin/SecretService.kt +++ b/components/secrets/backend/src/main/kotlin/SecretService.kt @@ -76,6 +76,11 @@ class SecretService( secretRepository.getByIdAndName(id, name) } + /** + * Get the value of a [secret]. Returns `null` if the value is not found. + */ + fun getSecretValue(secret: Secret): SecretValue? = secretStorage.readSecret(Path(secret.path)) + /** * List all secrets for the provided [hierarchy]. If there are secrets with the same name in different levels of the * hierarchy, only the one closest to the repository is returned. diff --git a/components/secrets/backend/src/test/kotlin/SecretServiceTest.kt b/components/secrets/backend/src/test/kotlin/SecretServiceTest.kt index 8aac9cb6f5..7ab07cac49 100644 --- a/components/secrets/backend/src/test/kotlin/SecretServiceTest.kt +++ b/components/secrets/backend/src/test/kotlin/SecretServiceTest.kt @@ -34,6 +34,7 @@ import org.eclipse.apoapsis.ortserver.model.OrganizationId import org.eclipse.apoapsis.ortserver.model.ProductId import org.eclipse.apoapsis.ortserver.model.RepositoryId import org.eclipse.apoapsis.ortserver.secrets.SecretStorage +import org.eclipse.apoapsis.ortserver.secrets.SecretValue import org.eclipse.apoapsis.ortserver.secrets.SecretsProviderFactoryForTesting import org.jetbrains.exposed.sql.Database @@ -55,6 +56,30 @@ class SecretServiceTest : WordSpec({ ) } + "getSecretValue" should { + "return the value of a secret" { + val secret = secretService.createSecret( + "secret", + "secret value", + "description", + OrganizationId(fixtures.organization.id) + ) + + secretService.getSecretValue(secret) shouldBe SecretValue("secret value") + } + + "return null if the value is not found" { + val secret = fixtures.secretRepository.create( + "path", + "name", + "description", + OrganizationId(fixtures.organization.id) + ) + + secretService.getSecretValue(secret) should beNull() + } + } + "listForHierarchy" should { "return an empty list if there are no secrets" { val hierarchy = Hierarchy( diff --git a/core/src/test/kotlin/api/RunsRouteIntegrationTest.kt b/core/src/test/kotlin/api/RunsRouteIntegrationTest.kt index 125ff30beb..c5979af9ce 100644 --- a/core/src/test/kotlin/api/RunsRouteIntegrationTest.kt +++ b/core/src/test/kotlin/api/RunsRouteIntegrationTest.kt @@ -102,6 +102,9 @@ import org.eclipse.apoapsis.ortserver.model.OrtRun import org.eclipse.apoapsis.ortserver.model.OrtRunStatus import org.eclipse.apoapsis.ortserver.model.PluginConfig import org.eclipse.apoapsis.ortserver.model.RepositoryType +import org.eclipse.apoapsis.ortserver.model.ResolvablePluginConfig +import org.eclipse.apoapsis.ortserver.model.ResolvableSecret +import org.eclipse.apoapsis.ortserver.model.SecretSource import org.eclipse.apoapsis.ortserver.model.Severity import org.eclipse.apoapsis.ortserver.model.UserDisplayName import org.eclipse.apoapsis.ortserver.model.repositories.OrtRunRepository @@ -633,9 +636,9 @@ class RunsRouteIntegrationTest : AbstractIntegrationTest({ ortRunId = ortRun.id, configuration = AdvisorJobConfiguration( config = mapOf( - "VulnerableCode" to PluginConfig( + "VulnerableCode" to ResolvablePluginConfig( options = mapOf("serverUrl" to "https://public.vulnerablecode.io"), - secrets = mapOf("apiKey" to "key") + secrets = mapOf("apiKey" to ResolvableSecret("key", SecretSource.ADMIN)) ) ) ) @@ -695,9 +698,9 @@ class RunsRouteIntegrationTest : AbstractIntegrationTest({ ortRunId = ortRun.id, configuration = AdvisorJobConfiguration( config = mapOf( - "VulnerableCode" to PluginConfig( + "VulnerableCode" to ResolvablePluginConfig( options = mapOf("serverUrl" to "https://public.vulnerablecode.io"), - secrets = mapOf("apiKey" to "key") + secrets = mapOf("apiKey" to ResolvableSecret("key", SecretSource.ADMIN)) ) ) ) @@ -761,9 +764,9 @@ class RunsRouteIntegrationTest : AbstractIntegrationTest({ ortRunId = ortRun.id, configuration = AdvisorJobConfiguration( config = mapOf( - "VulnerableCode" to PluginConfig( + "VulnerableCode" to ResolvablePluginConfig( options = mapOf("serverUrl" to "https://public.vulnerablecode.io"), - secrets = mapOf("apiKey" to "key") + secrets = mapOf("apiKey" to ResolvableSecret("key", SecretSource.ADMIN)) ) ) ) diff --git a/model/src/commonMain/kotlin/Hierarchy.kt b/model/src/commonMain/kotlin/Hierarchy.kt index 3255d19133..a4244fca8a 100644 --- a/model/src/commonMain/kotlin/Hierarchy.kt +++ b/model/src/commonMain/kotlin/Hierarchy.kt @@ -38,4 +38,12 @@ data class Hierarchy( /** The [Organization] the current repository and product belong to. */ val organization: Organization -) +) { + val compoundId: CompoundHierarchyId by lazy { + CompoundHierarchyId.forRepository( + organizationId = OrganizationId(organization.id), + productId = ProductId(product.id), + repositoryId = RepositoryId(repository.id) + ) + } +} diff --git a/model/src/commonMain/kotlin/JobConfigurations.kt b/model/src/commonMain/kotlin/JobConfigurations.kt index a6c25d7993..5d42994114 100644 --- a/model/src/commonMain/kotlin/JobConfigurations.kt +++ b/model/src/commonMain/kotlin/JobConfigurations.kt @@ -141,7 +141,7 @@ data class AdvisorJobConfiguration( /** * A map of plugin configurations that are specific to a concrete advisor. */ - val config: Map? = null, + val config: Map? = null, /** * Keep the worker alive after it has finished. This is useful for manual problem analysis directly @@ -180,7 +180,7 @@ data class ScannerJobConfiguration( /** * A map of plugin configurations that are specific to a concrete scanner. */ - val config: Map? = null, + val config: Map? = null, /** * Keep the worker alive after it has finished. This is useful for manual problem analysis directly @@ -245,7 +245,7 @@ data class ReporterJobConfiguration( /** * A map of configuration options that are specific to a concrete reporter. */ - val config: Map? = null, + val config: Map? = null, /** * Keep the worker alive after it has finished. This is useful for manual problem analysis directly diff --git a/model/src/commonMain/kotlin/ResolvablePluginConfig.kt b/model/src/commonMain/kotlin/ResolvablePluginConfig.kt new file mode 100644 index 0000000000..c8c9d7f1a3 --- /dev/null +++ b/model/src/commonMain/kotlin/ResolvablePluginConfig.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2025 The ORT Server Authors (See ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package org.eclipse.apoapsis.ortserver.model + +import kotlinx.serialization.Serializable + +/** A [PluginConfig] with information about from which source the secret values must be resolved. */ +@Serializable +data class ResolvablePluginConfig( + /** + * The configuration options of the plugin. + */ + val options: Options, + + /** + * The resolvable configuration secrets of the plugin. + */ + val secrets: Map +) diff --git a/model/src/commonMain/kotlin/ResolvableSecret.kt b/model/src/commonMain/kotlin/ResolvableSecret.kt new file mode 100644 index 0000000000..d0caba186a --- /dev/null +++ b/model/src/commonMain/kotlin/ResolvableSecret.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2025 The ORT Server Authors (See ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package org.eclipse.apoapsis.ortserver.model + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.KeepGeneratedSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonPrimitive + +@KeepGeneratedSerializer +@Serializable(with = ResolvableSecretSerializer::class) +data class ResolvableSecret( + /** The name of the secret. */ + val name: String, + + /** The source of the secret. */ + val source: SecretSource +) + +/** + * A custom deserializer for backward compatibility that allows deserializing a [ResolvableSecret] from either a string + * or an object. If the value is deserialized from a string, the source is set to [SecretSource.ADMIN] because that was + * the only available source before the introduction of [ResolvableSecret]. + */ +object ResolvableSecretSerializer : KSerializer { + private val delegate = ResolvableSecret.generatedSerializer() + + override val descriptor = SerialDescriptor( + serialName = "org.eclipse.apoapsis.ortserver.model.ResolvableSecret.Custom", + original = delegate.descriptor + ) + + override fun serialize(encoder: Encoder, value: ResolvableSecret) { + encoder.encodeSerializableValue(delegate, value) + } + + override fun deserialize(decoder: Decoder): ResolvableSecret { + val jsonDecoder = decoder as? JsonDecoder + ?: throw SerializationException("ResolvableSecret can only be deserialized from JSON.") + + return when (val element = jsonDecoder.decodeJsonElement()) { + is JsonPrimitive -> { + if (!element.isString) { + throw SerializationException("Expected string or object for ResolvableSecret.") + } + ResolvableSecret( + name = element.content, + source = SecretSource.ADMIN + ) + } + + else -> { + jsonDecoder.json.decodeFromJsonElement(delegate, element) + } + } + } +} diff --git a/model/src/commonMain/kotlin/SecretSource.kt b/model/src/commonMain/kotlin/SecretSource.kt new file mode 100644 index 0000000000..7dea477b27 --- /dev/null +++ b/model/src/commonMain/kotlin/SecretSource.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2025 The ORT Server Authors (See ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package org.eclipse.apoapsis.ortserver.model + +/** An enum class to describe the different sources for secrets. */ +enum class SecretSource { + /** The secret is resolved from the admin secrets. */ + ADMIN, + + /** The secret is resolved from the user secrets configured in the hierarchy. */ + USER +} diff --git a/model/src/jvmTest/kotlin/ResolvablePluginConfigTest.kt b/model/src/jvmTest/kotlin/ResolvablePluginConfigTest.kt new file mode 100644 index 0000000000..479631ff4c --- /dev/null +++ b/model/src/jvmTest/kotlin/ResolvablePluginConfigTest.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2025 The ORT Server Authors (See ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package org.eclipse.apoapsis.ortserver.model + +import io.kotest.core.spec.style.WordSpec +import io.kotest.matchers.shouldBe + +import kotlinx.serialization.json.Json + +class ResolvablePluginConfigTest : WordSpec({ + "deserialization" should { + "work for up-to-date input" { + val input = """ + { + "options": { + "key1": "value1", + "key2": "value2" + }, + "secrets": { + "secret1": { + "name": "admin-secret", + "source": "ADMIN" + }, + "secret2": { + "name": "user-secret", + "source": "USER" + } + } + } + """.trimIndent() + + Json.decodeFromString(input) shouldBe ResolvablePluginConfig( + options = mapOf( + "key1" to "value1", + "key2" to "value2" + ), + secrets = mapOf( + "secret1" to ResolvableSecret( + name = "admin-secret", + source = SecretSource.ADMIN + ), + "secret2" to ResolvableSecret( + name = "user-secret", + source = SecretSource.USER + ) + ) + ) + } + + "work for legacy input" { + val input = """ + { + "options": { + "key1": "value1", + "key2": "value2" + }, + "secrets": { + "secret1": "admin-secret", + "secret2": "user-secret" + } + } + """.trimIndent() + + Json.decodeFromString(input) shouldBe ResolvablePluginConfig( + options = mapOf( + "key1" to "value1", + "key2" to "value2" + ), + secrets = mapOf( + "secret1" to ResolvableSecret( + name = "admin-secret", + source = SecretSource.ADMIN + ), + "secret2" to ResolvableSecret( + name = "user-secret", + source = SecretSource.ADMIN + ) + ) + ) + } + } +}) diff --git a/model/src/jvmTest/kotlin/ResolvableSecretTest.kt b/model/src/jvmTest/kotlin/ResolvableSecretTest.kt new file mode 100644 index 0000000000..16d0e2dddc --- /dev/null +++ b/model/src/jvmTest/kotlin/ResolvableSecretTest.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2025 The ORT Server Authors (See ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package org.eclipse.apoapsis.ortserver.model + +import io.kotest.core.spec.style.WordSpec +import io.kotest.matchers.shouldBe + +import kotlinx.serialization.json.Json + +class ResolvableSecretTest : WordSpec({ + "deserialization" should { + "work for an object" { + Json.decodeFromString( + """ + { + "name": "admin-secret", + "source": "ADMIN" + } + """.trimIndent() + ) shouldBe ResolvableSecret(name = "admin-secret", source = SecretSource.ADMIN) + + Json.decodeFromString( + """ + { + "name": "user-secret", + "source": "USER" + } + """.trimIndent() + ) shouldBe ResolvableSecret(name = "user-secret", source = SecretSource.USER) + } + + "work for a string" { + val input = "\"admin-secret\"" + + Json.decodeFromString(input) shouldBe + ResolvableSecret(name = "admin-secret", source = SecretSource.ADMIN) + } + } +}) diff --git a/services/ort-run/src/test/kotlin/VulnerabilityServiceTest.kt b/services/ort-run/src/test/kotlin/VulnerabilityServiceTest.kt index 8eb24998ab..f89afa7224 100644 --- a/services/ort-run/src/test/kotlin/VulnerabilityServiceTest.kt +++ b/services/ort-run/src/test/kotlin/VulnerabilityServiceTest.kt @@ -39,6 +39,9 @@ import org.eclipse.apoapsis.ortserver.model.AdvisorJobConfiguration import org.eclipse.apoapsis.ortserver.model.JobConfigurations import org.eclipse.apoapsis.ortserver.model.OrtRun import org.eclipse.apoapsis.ortserver.model.PluginConfig +import org.eclipse.apoapsis.ortserver.model.ResolvablePluginConfig +import org.eclipse.apoapsis.ortserver.model.ResolvableSecret +import org.eclipse.apoapsis.ortserver.model.SecretSource import org.eclipse.apoapsis.ortserver.model.VulnerabilityFilters import org.eclipse.apoapsis.ortserver.model.VulnerabilityForRunsFilters import org.eclipse.apoapsis.ortserver.model.VulnerabilityRating @@ -1888,9 +1891,9 @@ class VulnerabilityServiceTest : WordSpec() { ortRunId = ortRun.id, configuration = AdvisorJobConfiguration( config = mapOf( - "VulnerableCode" to PluginConfig( + "VulnerableCode" to ResolvablePluginConfig( options = mapOf("serverUrl" to "https://public.vulnerablecode.io"), - secrets = mapOf("apiKey" to "key") + secrets = mapOf("apiKey" to ResolvableSecret("key", SecretSource.ADMIN)) ) ) ) @@ -1965,9 +1968,9 @@ class VulnerabilityServiceTest : WordSpec() { } private fun advisorConfig() = mapOf( - "VulnerableCode" to PluginConfig( + "VulnerableCode" to ResolvablePluginConfig( options = mapOf("serverUrl" to "https://public.vulnerablecode.io"), - secrets = mapOf("apiKey" to "key") + secrets = mapOf("apiKey" to ResolvableSecret("key", SecretSource.ADMIN)) ) ) } diff --git a/workers/advisor/build.gradle.kts b/workers/advisor/build.gradle.kts index 7de5dffabd..fac6df91f8 100644 --- a/workers/advisor/build.gradle.kts +++ b/workers/advisor/build.gradle.kts @@ -66,6 +66,7 @@ dependencies { testImplementation(testFixtures(projects.config.configSpi)) testImplementation(testFixtures(projects.dao)) + testImplementation(testFixtures(projects.secrets.secretsSpi)) testImplementation(testFixtures(projects.transport.transportSpi)) testImplementation(libs.koinTest) diff --git a/workers/advisor/src/test/kotlin/AdvisorEndpointTest.kt b/workers/advisor/src/test/kotlin/AdvisorEndpointTest.kt index fb1caeb3f9..41dc87b3a7 100644 --- a/workers/advisor/src/test/kotlin/AdvisorEndpointTest.kt +++ b/workers/advisor/src/test/kotlin/AdvisorEndpointTest.kt @@ -37,6 +37,7 @@ import org.eclipse.apoapsis.ortserver.dao.test.withMockDatabaseModule import org.eclipse.apoapsis.ortserver.model.orchestrator.AdvisorRequest import org.eclipse.apoapsis.ortserver.model.orchestrator.AdvisorWorkerError import org.eclipse.apoapsis.ortserver.model.orchestrator.AdvisorWorkerResult +import org.eclipse.apoapsis.ortserver.secrets.SecretsProviderFactoryForTesting import org.eclipse.apoapsis.ortserver.transport.AdvisorEndpoint import org.eclipse.apoapsis.ortserver.transport.Message import org.eclipse.apoapsis.ortserver.transport.MessageHeader @@ -159,7 +160,8 @@ class AdvisorEndpointTest : KoinTest, StringSpec() { "ADVISOR_RECEIVER_TRANSPORT_TYPE" to TEST_TRANSPORT_NAME, "ORCHESTRATOR_SENDER_TRANSPORT_TYPE" to TEST_TRANSPORT_NAME, "ADVISOR_SECRET_PROVIDER" to ConfigSecretProviderFactoryForTesting.NAME, - "VULNERABLE_CODE_API_KEY" to VULNERABLE_CODE_API_KEY + "VULNERABLE_CODE_API_KEY" to VULNERABLE_CODE_API_KEY, + "SECRETS_PROVIDER_NAME" to SecretsProviderFactoryForTesting.NAME ) withEnvironment(environment) { diff --git a/workers/advisor/src/test/kotlin/AdvisorRunnerTest.kt b/workers/advisor/src/test/kotlin/AdvisorRunnerTest.kt index 5232ca9c33..cc50796d63 100644 --- a/workers/advisor/src/test/kotlin/AdvisorRunnerTest.kt +++ b/workers/advisor/src/test/kotlin/AdvisorRunnerTest.kt @@ -32,6 +32,9 @@ import io.mockk.verify import org.eclipse.apoapsis.ortserver.model.AdvisorJobConfiguration import org.eclipse.apoapsis.ortserver.model.PluginConfig +import org.eclipse.apoapsis.ortserver.model.ResolvablePluginConfig +import org.eclipse.apoapsis.ortserver.model.ResolvableSecret +import org.eclipse.apoapsis.ortserver.model.SecretSource import org.eclipse.apoapsis.ortserver.services.ortrun.mapToOrt import org.eclipse.apoapsis.ortserver.workers.common.context.WorkerContext @@ -69,16 +72,22 @@ class AdvisorRunnerTest : WordSpec({ val vulnerableCodeFactory = mockAdviceProviderFactory("VulnerableCode") mockAdvisorAll(listOf(osvFactory, vulnerableCodeFactory)) - val osvSecretRefs = mapOf("secret1" to "passRef1", "secret2" to "passRef2") + val osvSecretRefs = mapOf( + "secret1" to ResolvableSecret("passRef1", SecretSource.ADMIN), + "secret2" to ResolvableSecret("passRef2", SecretSource.ADMIN) + ) val osvSecrets = mapOf("secret1" to "pass1", "secret2" to "pass2") - val osvConfig = PluginConfig( + val osvConfig = ResolvablePluginConfig( options = mapOf("option1" to "value1", "option2" to "value2"), secrets = osvSecretRefs ) - val vulnerableCodeSecretRefs = mapOf("secret3" to "passRef3", "secret4" to "passRef4") + val vulnerableCodeSecretRefs = mapOf( + "secret3" to ResolvableSecret("passRef3", SecretSource.ADMIN), + "secret4" to ResolvableSecret("passRef4", SecretSource.ADMIN) + ) val vulnerableCodeSecrets = mapOf("secret3" to "pass3", "secret4" to "pass4") - val vulnerableCodeConfig = PluginConfig( + val vulnerableCodeConfig = ResolvablePluginConfig( options = mapOf("option3" to "value3", "option4" to "value4"), secrets = vulnerableCodeSecretRefs ) @@ -92,8 +101,11 @@ class AdvisorRunnerTest : WordSpec({ ) val resolvedPluginConfig = mapOf( - "OSV" to osvConfig.copy(secrets = osvSecrets), - "VulnerableCode" to vulnerableCodeConfig.copy(secrets = vulnerableCodeSecrets) + "OSV" to PluginConfig(options = osvConfig.options, secrets = osvSecrets), + "VulnerableCode" to PluginConfig( + options = vulnerableCodeConfig.options, + secrets = vulnerableCodeSecrets + ) ) val context = mockContext(jobConfig, resolvedPluginConfig) @@ -110,8 +122,13 @@ class AdvisorRunnerTest : WordSpec({ ) verify(exactly = 1) { - osvFactory.create(osvConfig.copy(secrets = osvSecrets).mapToOrt()) - vulnerableCodeFactory.create(vulnerableCodeConfig.copy(secrets = vulnerableCodeSecrets).mapToOrt()) + osvFactory.create(PluginConfig(options = osvConfig.options, secrets = osvSecrets).mapToOrt()) + vulnerableCodeFactory.create( + PluginConfig( + options = vulnerableCodeConfig.options, + secrets = vulnerableCodeSecrets + ).mapToOrt() + ) } } } diff --git a/workers/common/src/main/kotlin/common/Extensions.kt b/workers/common/src/main/kotlin/common/Extensions.kt index dc08e3bf8a..82a4b28bff 100644 --- a/workers/common/src/main/kotlin/common/Extensions.kt +++ b/workers/common/src/main/kotlin/common/Extensions.kt @@ -26,6 +26,7 @@ import org.eclipse.apoapsis.ortserver.config.ConfigManager import org.eclipse.apoapsis.ortserver.config.Context import org.eclipse.apoapsis.ortserver.config.Path import org.eclipse.apoapsis.ortserver.model.PluginConfig +import org.eclipse.apoapsis.ortserver.model.ResolvablePluginConfig import org.eclipse.apoapsis.ortserver.services.config.AdminConfigService import org.eclipse.apoapsis.ortserver.workers.common.context.WorkerContext @@ -41,9 +42,9 @@ val logger: Logger = LoggerFactory.getLogger(ConfigManager::class.java) /** * Map the entries of all [PluginConfig.options] in this map using the provided [transform] function. */ -fun Map.mapOptions( +fun Map.mapOptions( transform: (Map.Entry) -> String -): Map = +): Map = mapValues { (_, pluginConfig) -> pluginConfig.copy(options = pluginConfig.options.mapValues(transform)) } /** diff --git a/workers/common/src/main/kotlin/common/context/WorkerContext.kt b/workers/common/src/main/kotlin/common/context/WorkerContext.kt index 496decd98f..d4705361c0 100644 --- a/workers/common/src/main/kotlin/common/context/WorkerContext.kt +++ b/workers/common/src/main/kotlin/common/context/WorkerContext.kt @@ -28,6 +28,7 @@ import org.eclipse.apoapsis.ortserver.model.InfrastructureService import org.eclipse.apoapsis.ortserver.model.OrtRun import org.eclipse.apoapsis.ortserver.model.PluginConfig import org.eclipse.apoapsis.ortserver.model.ProviderPluginConfiguration +import org.eclipse.apoapsis.ortserver.model.ResolvablePluginConfig import org.eclipse.apoapsis.ortserver.model.Secret import org.eclipse.apoapsis.ortserver.workers.common.auth.AuthenticationListener import org.eclipse.apoapsis.ortserver.workers.common.auth.CredentialResolverFun @@ -83,7 +84,7 @@ interface WorkerContext : AutoCloseable { * in contrast to [resolveSecrets], this function deals with secrets from the configuration instead of secrets * managed on behalf of customers. */ - suspend fun resolvePluginConfigSecrets(config: Map?): Map + suspend fun resolvePluginConfigSecrets(config: Map?): Map /** * Resolve all the secrets referenced by the given [config]. If [config] is not *null*, obtain the referenced diff --git a/workers/common/src/main/kotlin/common/context/WorkerContextFactory.kt b/workers/common/src/main/kotlin/common/context/WorkerContextFactory.kt index a1c2a835f7..59bde1f375 100644 --- a/workers/common/src/main/kotlin/common/context/WorkerContextFactory.kt +++ b/workers/common/src/main/kotlin/common/context/WorkerContextFactory.kt @@ -19,6 +19,7 @@ package org.eclipse.apoapsis.ortserver.workers.common.context +import org.eclipse.apoapsis.ortserver.components.secrets.SecretService import org.eclipse.apoapsis.ortserver.config.ConfigManager import org.eclipse.apoapsis.ortserver.model.repositories.OrtRunRepository import org.eclipse.apoapsis.ortserver.model.repositories.RepositoryRepository @@ -37,7 +38,10 @@ class WorkerContextFactory( private val ortRunRepository: OrtRunRepository, /** The repository for repository entities. */ - private val repositoryRepository: RepositoryRepository + private val repositoryRepository: RepositoryRepository, + + /** The service for accessing secrets. */ + private val secretService: SecretService ) { /** * Create a new [WorkerContext] for the given [ID of an ORT run][ortRunId] and execute the given [block] passing @@ -62,6 +66,6 @@ class WorkerContextFactory( val ortConfig = WorkerOrtConfig.create(configManager) ortConfig.setUpOrtEnvironment() - return WorkerContextImpl(configManager, ortRunRepository, repositoryRepository, ortRunId) + return WorkerContextImpl(configManager, ortRunRepository, repositoryRepository, ortRunId, secretService) } } diff --git a/workers/common/src/main/kotlin/common/context/WorkerContextImpl.kt b/workers/common/src/main/kotlin/common/context/WorkerContextImpl.kt index 9f2d828786..f998179173 100644 --- a/workers/common/src/main/kotlin/common/context/WorkerContextImpl.kt +++ b/workers/common/src/main/kotlin/common/context/WorkerContextImpl.kt @@ -31,6 +31,7 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext +import org.eclipse.apoapsis.ortserver.components.secrets.SecretService import org.eclipse.apoapsis.ortserver.config.ConfigManager import org.eclipse.apoapsis.ortserver.config.Context import org.eclipse.apoapsis.ortserver.config.Path as ConfigPath @@ -39,11 +40,12 @@ import org.eclipse.apoapsis.ortserver.model.InfrastructureService import org.eclipse.apoapsis.ortserver.model.OrtRun import org.eclipse.apoapsis.ortserver.model.PluginConfig import org.eclipse.apoapsis.ortserver.model.ProviderPluginConfiguration +import org.eclipse.apoapsis.ortserver.model.ResolvablePluginConfig import org.eclipse.apoapsis.ortserver.model.Secret +import org.eclipse.apoapsis.ortserver.model.SecretSource import org.eclipse.apoapsis.ortserver.model.repositories.OrtRunRepository import org.eclipse.apoapsis.ortserver.model.repositories.RepositoryRepository -import org.eclipse.apoapsis.ortserver.secrets.Path -import org.eclipse.apoapsis.ortserver.secrets.SecretStorage +import org.eclipse.apoapsis.ortserver.utils.logging.runBlocking import org.eclipse.apoapsis.ortserver.workers.common.auth.AuthenticationInfo import org.eclipse.apoapsis.ortserver.workers.common.auth.AuthenticationListener import org.eclipse.apoapsis.ortserver.workers.common.auth.CredentialResolverFun @@ -75,12 +77,12 @@ internal class WorkerContextImpl( /** The ID of the current ORT run. */ private val ortRunId: Long, -) : WorkerContext { - /** The object for accessing secrets. */ - private val secretStorage by lazy { SecretStorage.createStorage(configManager) } + /** The service for accessing secrets. */ + private val secretService: SecretService +) : WorkerContext { /** A cache for the secrets that have already been loaded. */ - private val secretsCache = ConcurrentHashMap>() + private val secretsCache = ConcurrentHashMap>() /** A cache for the configuration secrets that have already been loaded. */ private val configSecretsCache = ConcurrentHashMap>() @@ -113,6 +115,10 @@ internal class WorkerContextImpl( repositoryRepository.getHierarchy(ortRun.repositoryId) } + private val hierarchySecrets by lazy { + runBlocking { secretService.listForHierarchy(hierarchy) }.associateBy { it.name } + } + override val credentialResolverFun: CredentialResolverFun get() = { secret -> refCredentialResolverFun.get().invoke(secret) @@ -125,20 +131,31 @@ internal class WorkerContextImpl( private val currentContext by lazy { ortRun.resolvedJobConfigContext?.let(::Context) } override suspend fun resolveSecret(secret: Secret): String = - singleTransform(secret, secretsCache, this::resolveSecret, ::extractSecretKey) + singleTransform(secret, secretsCache, ::resolveSecretValue) { it } - override suspend fun resolveSecrets(vararg secrets: Secret): Map { - return parallelTransform(secrets.toList(), secretsCache, this::resolveSecret, ::extractSecretKey) - } + override suspend fun resolveSecrets(vararg secrets: Secret): Map = + parallelTransform(secrets.toList(), secretsCache, ::resolveSecretValue) { it } override suspend fun resolvePluginConfigSecrets( - config: Map? + config: Map? ): Map = config?.let { c -> - val secrets = c.values.flatMap { it.secrets.values } - val resolvedSecrets = parallelTransform(secrets, configSecretsCache, this::resolveConfigSecret) { it } + val configSecrets = c.values.flatMap { pluginConfig -> + pluginConfig.secrets.values.filter { it.source == SecretSource.ADMIN }.map { it.name } + } + val userSecrets = c.values.flatMap { pluginConfig -> + pluginConfig.secrets.values.filter { it.source == SecretSource.USER }.map { + hierarchySecrets[it.name] + ?: error("Could not find secret '${it.name}' in hierarchy '${hierarchy.compoundId}'.") + } + } + + val resolvedConfigSecrets = + parallelTransform(configSecrets, configSecretsCache, ::resolveConfigSecret) { it } + val resolvedUserSecrets = parallelTransform(userSecrets, secretsCache, ::resolveSecretValue) { it } + .mapKeys { it.key.name } - c.mapValues { (_, pluginConfig) -> pluginConfig.resolveSecrets(resolvedSecrets) } + c.mapValues { (_, pluginConfig) -> pluginConfig.resolveSecrets(resolvedConfigSecrets, resolvedUserSecrets) } }.orEmpty() override suspend fun resolveProviderPluginConfigSecrets( @@ -259,10 +276,10 @@ internal class WorkerContextImpl( } /** - * Resolve the secret with the given [path] using the [SecretStorage] owned by this instance. + * Resolve the value of the provided [secret] using the [secretService]. */ - private fun resolveSecret(path: String): String = - secretStorage.getSecret(Path(path)).value + private fun resolveSecretValue(secret: Secret): String = + secretService.getSecretValue(secret)?.value ?: error("Could not resolve secret at path '${secret.path}'") /** * Resolve the given [secret] from the configuration manager. @@ -283,11 +300,6 @@ internal class WorkerContextImpl( } } -/** - * A key extraction function for [Secret]s. As key for the given [secret] its path is used. - */ -private fun extractSecretKey(secret: Secret): String = secret.path - /** * Return a key extraction function for configuration files that are downloaded to the given [directory] and optionally * renamed to the given [targetName]. The resulting function ensures that all relevant components are reflected in @@ -299,11 +311,21 @@ private fun extractDownloadFileKey(directory: String, targetName: String?): (Con } /** - * Return a [PluginConfig] whose secrets are resolved according to the given map with [secretValues]. + * Return a [PluginConfig] whose secrets are resolved according to the given map with [configSecretValues] and + * [userSecretValues]. */ -private fun PluginConfig.resolveSecrets(secretValues: Map): PluginConfig { - val resolvedSecrets = secrets.mapValues { e -> secretValues.getValue(e.value) } - return copy(secrets = resolvedSecrets) +private fun ResolvablePluginConfig.resolveSecrets( + configSecretValues: Map, + userSecretValues: Map +): PluginConfig { + val resolvedSecrets = secrets.mapValues { (_, resolvableSecret) -> + when (resolvableSecret.source) { + SecretSource.ADMIN -> configSecretValues.getValue(resolvableSecret.name) + SecretSource.USER -> userSecretValues.getValue(resolvableSecret.name) + } + } + + return PluginConfig(options, resolvedSecrets) } /** diff --git a/workers/common/src/main/kotlin/common/context/WorkerContextModule.kt b/workers/common/src/main/kotlin/common/context/WorkerContextModule.kt index dd017dd642..4a1428c642 100644 --- a/workers/common/src/main/kotlin/common/context/WorkerContextModule.kt +++ b/workers/common/src/main/kotlin/common/context/WorkerContextModule.kt @@ -19,10 +19,14 @@ package org.eclipse.apoapsis.ortserver.workers.common.context +import org.eclipse.apoapsis.ortserver.components.secrets.SecretService import org.eclipse.apoapsis.ortserver.dao.repositories.ortrun.DaoOrtRunRepository import org.eclipse.apoapsis.ortserver.dao.repositories.repository.DaoRepositoryRepository +import org.eclipse.apoapsis.ortserver.dao.repositories.secret.DaoSecretRepository import org.eclipse.apoapsis.ortserver.model.repositories.OrtRunRepository import org.eclipse.apoapsis.ortserver.model.repositories.RepositoryRepository +import org.eclipse.apoapsis.ortserver.model.repositories.SecretRepository +import org.eclipse.apoapsis.ortserver.secrets.SecretStorage import org.eclipse.apoapsis.ortserver.services.config.AdminConfigService import org.koin.core.module.Module @@ -37,6 +41,12 @@ import org.koin.dsl.module fun workerContextModule(): Module = module { single { DaoOrtRunRepository(get()) } single { DaoRepositoryRepository(get()) } + single { DaoSecretRepository(get()) } + + single { + val secretStorage = SecretStorage.createStorage(get()) + SecretService(get(), get(), secretStorage) + } singleOf(::WorkerContextFactory) singleOf(::AdminConfigService) diff --git a/workers/common/src/test/kotlin/common/ExtensionsTest.kt b/workers/common/src/test/kotlin/common/ExtensionsTest.kt index d94dd8269a..90891c6a44 100644 --- a/workers/common/src/test/kotlin/common/ExtensionsTest.kt +++ b/workers/common/src/test/kotlin/common/ExtensionsTest.kt @@ -36,7 +36,9 @@ import org.eclipse.apoapsis.ortserver.config.ConfigManager import org.eclipse.apoapsis.ortserver.config.Context import org.eclipse.apoapsis.ortserver.config.Path import org.eclipse.apoapsis.ortserver.model.OrtRun -import org.eclipse.apoapsis.ortserver.model.PluginConfig +import org.eclipse.apoapsis.ortserver.model.ResolvablePluginConfig +import org.eclipse.apoapsis.ortserver.model.ResolvableSecret +import org.eclipse.apoapsis.ortserver.model.SecretSource import org.eclipse.apoapsis.ortserver.workers.common.context.WorkerContext class ExtensionsTest : WordSpec({ @@ -61,22 +63,22 @@ class ExtensionsTest : WordSpec({ "mapOptions" should { "apply the transform to all option entries" { val pluginConfigs = mapOf( - "plugin1" to PluginConfig( + "plugin1" to ResolvablePluginConfig( options = mapOf("key1" to "value1", "key2" to "value2"), secrets = emptyMap() ), - "plugin2" to PluginConfig( + "plugin2" to ResolvablePluginConfig( options = mapOf("key3" to "value3", "key4" to "value4"), secrets = emptyMap() ) ) pluginConfigs.mapOptions { it.key + it.value } should containExactly( - "plugin1" to PluginConfig( + "plugin1" to ResolvablePluginConfig( options = mapOf("key1" to "key1value1", "key2" to "key2value2"), secrets = emptyMap() ), - "plugin2" to PluginConfig( + "plugin2" to ResolvablePluginConfig( options = mapOf("key3" to "key3value3", "key4" to "key4value4"), secrets = emptyMap() ) @@ -85,16 +87,22 @@ class ExtensionsTest : WordSpec({ "not apply the transform to the secrets" { val pluginConfigs = mapOf( - "plugin1" to PluginConfig( + "plugin1" to ResolvablePluginConfig( options = emptyMap(), - secrets = mapOf("key1" to "value1", "key2" to "value2") + secrets = mapOf( + "key1" to ResolvableSecret("value1", SecretSource.ADMIN), + "key2" to ResolvableSecret("value2", SecretSource.ADMIN) + ) ) ) pluginConfigs.mapOptions { it.key + it.value } should containExactly( - "plugin1" to PluginConfig( + "plugin1" to ResolvablePluginConfig( options = emptyMap(), - secrets = mapOf("key1" to "value1", "key2" to "value2") + secrets = mapOf( + "key1" to ResolvableSecret("value1", SecretSource.ADMIN), + "key2" to ResolvableSecret("value2", SecretSource.ADMIN) + ) ) ) } diff --git a/workers/common/src/test/kotlin/common/context/WorkerContextFactoryTest.kt b/workers/common/src/test/kotlin/common/context/WorkerContextTest.kt similarity index 81% rename from workers/common/src/test/kotlin/common/context/WorkerContextFactoryTest.kt rename to workers/common/src/test/kotlin/common/context/WorkerContextTest.kt index dae40b96ae..e206512020 100644 --- a/workers/common/src/test/kotlin/common/context/WorkerContextFactoryTest.kt +++ b/workers/common/src/test/kotlin/common/context/WorkerContextTest.kt @@ -48,18 +48,33 @@ import io.mockk.slot import io.mockk.unmockkAll import io.mockk.verify +import org.eclipse.apoapsis.ortserver.components.secrets.SecretService import org.eclipse.apoapsis.ortserver.config.ConfigFileProviderFactoryForTesting import org.eclipse.apoapsis.ortserver.config.ConfigManager import org.eclipse.apoapsis.ortserver.config.ConfigSecretProviderFactoryForTesting import org.eclipse.apoapsis.ortserver.config.Path +import org.eclipse.apoapsis.ortserver.dao.test.mockkTransaction import org.eclipse.apoapsis.ortserver.model.Hierarchy import org.eclipse.apoapsis.ortserver.model.InfrastructureService +import org.eclipse.apoapsis.ortserver.model.Organization +import org.eclipse.apoapsis.ortserver.model.OrganizationId import org.eclipse.apoapsis.ortserver.model.OrtRun import org.eclipse.apoapsis.ortserver.model.PluginConfig +import org.eclipse.apoapsis.ortserver.model.Product +import org.eclipse.apoapsis.ortserver.model.ProductId import org.eclipse.apoapsis.ortserver.model.ProviderPluginConfiguration +import org.eclipse.apoapsis.ortserver.model.Repository +import org.eclipse.apoapsis.ortserver.model.RepositoryId +import org.eclipse.apoapsis.ortserver.model.RepositoryType +import org.eclipse.apoapsis.ortserver.model.ResolvablePluginConfig +import org.eclipse.apoapsis.ortserver.model.ResolvableSecret import org.eclipse.apoapsis.ortserver.model.Secret +import org.eclipse.apoapsis.ortserver.model.SecretSource import org.eclipse.apoapsis.ortserver.model.repositories.OrtRunRepository import org.eclipse.apoapsis.ortserver.model.repositories.RepositoryRepository +import org.eclipse.apoapsis.ortserver.model.repositories.SecretRepository +import org.eclipse.apoapsis.ortserver.model.util.ListQueryParameters +import org.eclipse.apoapsis.ortserver.model.util.ListQueryResult import org.eclipse.apoapsis.ortserver.secrets.Path as SecretPath import org.eclipse.apoapsis.ortserver.secrets.SecretStorage import org.eclipse.apoapsis.ortserver.secrets.SecretValue @@ -71,11 +86,15 @@ import org.eclipse.apoapsis.ortserver.workers.common.auth.OrtServerAuthenticator import org.ossreviewtoolkit.utils.ort.OrtAuthenticator -class WorkerContextFactoryTest : WordSpec({ +class WorkerContextTest : WordSpec({ + lateinit var helper: ContextFactoryTestHelper + beforeEach { mockkObject(OrtServerAuthenticator) every { OrtServerAuthenticator.install(any()) } returns mockk(relaxed = true) + + helper = ContextFactoryTestHelper() } afterEach { @@ -84,8 +103,6 @@ class WorkerContextFactoryTest : WordSpec({ "ortRun" should { "return the OrtRun object" { - val helper = ContextFactoryTestHelper() - val run = helper.expectRunRequest() val context = helper.context() @@ -94,8 +111,6 @@ class WorkerContextFactoryTest : WordSpec({ } "throw an exception if the run ID cannot be resolved" { - val helper = ContextFactoryTestHelper() - every { helper.ortRunRepository.get(any()) } returns null val context = helper.context() @@ -109,7 +124,6 @@ class WorkerContextFactoryTest : WordSpec({ "hierarchy" should { "return the hierarchy of the current repository" { val repositoryId = 20230607144801L - val helper = ContextFactoryTestHelper() val run = helper.expectRunRequest() every { run.repositoryId } returns repositoryId @@ -125,7 +139,6 @@ class WorkerContextFactoryTest : WordSpec({ "createTempDir" should { "return a temporary directory" { - val helper = ContextFactoryTestHelper() helper.context().use { context -> val dir1 = context.createTempDir() val dir2 = context.createTempDir() @@ -136,7 +149,6 @@ class WorkerContextFactoryTest : WordSpec({ } "remove the content of the temporary directory when the context is closed" { - val helper = ContextFactoryTestHelper() val tempDir = helper.context().use { context -> val dir = context.createTempDir() @@ -156,7 +168,6 @@ class WorkerContextFactoryTest : WordSpec({ "resolve a secret" { val secret = createSecret(SecretsProviderFactoryForTesting.PASSWORD_PATH.path) - val helper = ContextFactoryTestHelper() val context = helper.context() context.resolveSecret(secret) shouldBe SecretsProviderFactoryForTesting.PASSWORD_SECRET.value @@ -165,14 +176,13 @@ class WorkerContextFactoryTest : WordSpec({ "cache the value of a secret that has been resolved" { val secret = createSecret(SecretsProviderFactoryForTesting.SERVICE_PATH.path) - val helper = ContextFactoryTestHelper() val context = helper.context() context.resolveSecret(secret) val secretsProvider = SecretsProviderFactoryForTesting.instance() secretsProvider.writeSecret( SecretsProviderFactoryForTesting.SERVICE_PATH, - org.eclipse.apoapsis.ortserver.secrets.SecretValue("changedValue") + SecretValue("changedValue") ) context.resolveSecret(secret) shouldBe SecretsProviderFactoryForTesting.SERVICE_SECRET.value @@ -185,7 +195,6 @@ class WorkerContextFactoryTest : WordSpec({ val secret2 = createSecret(SecretsProviderFactoryForTesting.SERVICE_PATH.path) val secret3 = createSecret(SecretsProviderFactoryForTesting.TOKEN_PATH.path) - val helper = ContextFactoryTestHelper() val context = helper.context() val secretValues = context.resolveSecrets(secret1, secret2, secret3) @@ -201,7 +210,6 @@ class WorkerContextFactoryTest : WordSpec({ val secret1 = createSecret(SecretsProviderFactoryForTesting.PASSWORD_PATH.path) val secret2 = createSecret(SecretsProviderFactoryForTesting.SERVICE_PATH.path) - val helper = ContextFactoryTestHelper() val context = helper.context() context.resolveSecrets(secret1, secret2) @@ -209,7 +217,7 @@ class WorkerContextFactoryTest : WordSpec({ val secretsProvider = SecretsProviderFactoryForTesting.instance() secretsProvider.writeSecret( SecretsProviderFactoryForTesting.SERVICE_PATH, - org.eclipse.apoapsis.ortserver.secrets.SecretValue("changedValue") + SecretValue("changedValue") ) context.resolveSecret(secret1) shouldBe SecretsProviderFactoryForTesting.PASSWORD_SECRET.value @@ -219,7 +227,6 @@ class WorkerContextFactoryTest : WordSpec({ "downloadConfigurationFile" should { "download a single configuration file" { val dir = tempdir() - val helper = ContextFactoryTestHelper() helper.expectRunRequest() val context = helper.context() @@ -232,7 +239,6 @@ class WorkerContextFactoryTest : WordSpec({ "allow renaming a configuration file" { val dir = tempdir() val targetName = "my-config.txt" - val helper = ContextFactoryTestHelper() helper.expectRunRequest() val context = helper.context() @@ -244,7 +250,6 @@ class WorkerContextFactoryTest : WordSpec({ "cache files that have already been downloaded" { val dir = tempdir() - val helper = ContextFactoryTestHelper() helper.expectRunRequest() val context = helper.context() @@ -256,7 +261,6 @@ class WorkerContextFactoryTest : WordSpec({ "not cache downloaded files if they use different names" { val dir = tempdir() - val helper = ContextFactoryTestHelper() helper.expectRunRequest() val context = helper.context() @@ -269,7 +273,6 @@ class WorkerContextFactoryTest : WordSpec({ "not cache downloaded files if they use different target directories" { val dir1 = tempdir() val dir2 = tempdir() - val helper = ContextFactoryTestHelper() helper.expectRunRequest() val context = helper.context() @@ -282,7 +285,6 @@ class WorkerContextFactoryTest : WordSpec({ "downloadConfigurationFiles" should { "download multiple configuration files" { - val helper = ContextFactoryTestHelper() helper.expectRunRequest() helper.context().use { context -> @@ -299,7 +301,6 @@ class WorkerContextFactoryTest : WordSpec({ "cache files that have already been downloaded" { val dir = tempdir() - val helper = ContextFactoryTestHelper() helper.expectRunRequest() helper.context().use { context -> @@ -314,7 +315,6 @@ class WorkerContextFactoryTest : WordSpec({ "downloadConfigurationDirectory" should { "download all files in a configuration directory" { - val helper = ContextFactoryTestHelper() helper.expectRunRequest() helper.context().use { context -> @@ -331,32 +331,92 @@ class WorkerContextFactoryTest : WordSpec({ "resolvePluginConfigSecrets" should { "return an empty Map for null input" { - val helper = ContextFactoryTestHelper() - val resolvedConfig = helper.context().resolvePluginConfigSecrets(null) resolvedConfig should beEmptyMap() } "return plugin configurations with resolved secrets" { - val pluginConfig1 = PluginConfig( + val hierarchy = Hierarchy( + repository = Repository( + id = 1L, + productId = 2L, + organizationId = 3L, + type = RepositoryType.GIT, + url = "https://example.org/repo.git" + ), + product = Product(id = 2L, organizationId = 3L, name = "prod"), + organization = Organization(id = 3L, name = "org") + ) + + val run = helper.expectRunRequest() + every { run.repositoryId } returns hierarchy.repository.id + + every { helper.repositoryRepository.getHierarchy(hierarchy.repository.id) } returns hierarchy + + every { helper.secretRepository.listForId(OrganizationId(hierarchy.organization.id)) } returns + ListQueryResult( + data = listOf( + Secret( + id = 1L, + path = "serviceUser", + name = "serviceUser", + description = null, + organizationId = hierarchy.organization.id, + productId = null, + repositoryId = null + ) + ), + params = ListQueryParameters.DEFAULT, + totalCount = 1 + ) + every { helper.secretRepository.listForId(ProductId(hierarchy.product.id)) } returns + ListQueryResult(emptyList(), ListQueryParameters.DEFAULT, 0) + every { helper.secretRepository.listForId(RepositoryId(hierarchy.repository.id)) } returns + ListQueryResult( + data = listOf( + Secret( + id = 1L, + path = "servicePassword", + name = "servicePassword", + description = null, + organizationId = null, + productId = null, + repositoryId = hierarchy.repository.id + ) + ), + params = ListQueryParameters.DEFAULT, + totalCount = 1 + ) + + SecretsProviderFactoryForTesting.instance().run { + writeSecret(SecretPath("serviceUser"), SecretValue("svcUser")) + writeSecret(SecretPath("servicePassword"), SecretValue("svcPass")) + } + + val pluginConfig1 = ResolvablePluginConfig( options = mapOf("plugin1Option1" to "v1", "plugin1Option2" to "v2"), - secrets = mapOf("plugin1User" to "dbUser", "plugin1Password" to "dbPassword") + secrets = mapOf( + "plugin1User" to ResolvableSecret("dbUser", SecretSource.ADMIN), + "plugin1Password" to ResolvableSecret("dbPassword", SecretSource.ADMIN) + ) ) - val pluginConfig2 = PluginConfig( + val pluginConfig2 = ResolvablePluginConfig( options = mapOf("plugin2Option" to "v3"), secrets = mapOf( - "plugin2ServiceUser" to "serviceUser", - "plugin2ServicePassword" to "servicePassword", - "plugin2DBAccess" to "dbPassword" + "plugin2ServiceUser" to ResolvableSecret("serviceUser", SecretSource.USER), + "plugin2ServicePassword" to ResolvableSecret("servicePassword", SecretSource.USER), + "plugin2DBAccess" to ResolvableSecret("dbPassword", SecretSource.ADMIN) ) ) val config = mapOf("p1" to pluginConfig1, "p2" to pluginConfig2) - val resolvedConfig1 = pluginConfig1.copy( + val resolvedConfig1 = PluginConfig( + options = pluginConfig1.options, secrets = mapOf("plugin1User" to "scott", "plugin1Password" to "tiger") ) - val resolvedConfig2 = pluginConfig2.copy( + val resolvedConfig2 = PluginConfig( + options = pluginConfig2.options, secrets = mapOf( "plugin2ServiceUser" to "svcUser", "plugin2ServicePassword" to "svcPass", @@ -365,9 +425,9 @@ class WorkerContextFactoryTest : WordSpec({ ) val expectedConfig = mapOf("p1" to resolvedConfig1, "p2" to resolvedConfig2) - val helper = ContextFactoryTestHelper() - - val resolvedConfig = helper.context().resolvePluginConfigSecrets(config) + val resolvedConfig = mockkTransaction { + helper.context().resolvePluginConfigSecrets(config) + } resolvedConfig shouldBe expectedConfig } @@ -375,8 +435,6 @@ class WorkerContextFactoryTest : WordSpec({ "resolveProviderPluginConfigSecrets" should { "return an empty Map for null input" { - val helper = ContextFactoryTestHelper() - val resolvedConfig = helper.context().resolveProviderPluginConfigSecrets(null) resolvedConfig should beEmpty() @@ -411,8 +469,6 @@ class WorkerContextFactoryTest : WordSpec({ ) val expectedConfig = listOf(resolvedConfig1, resolvedConfig2) - val helper = ContextFactoryTestHelper() - val resolvedConfig = helper.context().resolveProviderPluginConfigSecrets(config) resolvedConfig shouldContainExactly expectedConfig @@ -427,7 +483,6 @@ class WorkerContextFactoryTest : WordSpec({ } every { WorkerOrtConfig.create(config) } returns workerOrtConfigMock - val helper = ContextFactoryTestHelper() helper.context() verify { @@ -438,8 +493,6 @@ class WorkerContextFactoryTest : WordSpec({ "withContext" should { "properly close the context after the block has been executed" { - val helper = ContextFactoryTestHelper() - val tempDir = helper.factory.withContext(RUN_ID) { context -> context.createTempDir().also { it.exists() shouldBe true @@ -454,7 +507,6 @@ class WorkerContextFactoryTest : WordSpec({ "uninstall the ORT Server authenticator" { mockkObject(OrtAuthenticator) - val helper = ContextFactoryTestHelper() helper.factory.withContext(RUN_ID) { } verify { @@ -471,7 +523,6 @@ class WorkerContextFactoryTest : WordSpec({ } every { OrtServerAuthenticator.install(any()) } returns authenticator - val helper = ContextFactoryTestHelper() val context = helper.context() // Make sure the secrets provider is initialized. @@ -532,7 +583,6 @@ class WorkerContextFactoryTest : WordSpec({ } "pass a correct resolver function to the authenticator" { - val helper = ContextFactoryTestHelper() helper.context() val slotResolverFun = slot() @@ -550,7 +600,6 @@ class WorkerContextFactoryTest : WordSpec({ "credentialsResolverFunc" should { "always fail if there are no current services" { - val helper = ContextFactoryTestHelper() val context = helper.context() val resolverFun = context.credentialResolverFun @@ -561,7 +610,6 @@ class WorkerContextFactoryTest : WordSpec({ } "resolve a secret from an active infrastructure service" { - val helper = ContextFactoryTestHelper() val context = helper.context() // Make sure the secrets provider is initialized. @@ -593,12 +641,8 @@ class WorkerContextFactoryTest : WordSpec({ } "be aware of later changes of authentication data" { - val helper = ContextFactoryTestHelper() val context = helper.context() - // Make sure the secrets provider is initialized. - context.resolveSecret(createSecret(SecretsProviderFactoryForTesting.PASSWORD_PATH.path)) - val resolverFun = context.credentialResolverFun val secretsProvider = SecretsProviderFactoryForTesting.instance() @@ -667,16 +711,27 @@ private fun createSecret(path: String): Secret = /** * A test helper class managing a [WorkerContextFactory] instance and its dependencies. */ -private class ContextFactoryTestHelper( +private class ContextFactoryTestHelper { /** Mock for the [OrtRunRepository]. */ - val ortRunRepository: OrtRunRepository = mockk(), + val ortRunRepository: OrtRunRepository = mockk() /** Mock for the [RepositoryRepository]. */ - val repositoryRepository: RepositoryRepository = mockk(), + val repositoryRepository: RepositoryRepository = mockk() + + /** Mock for the [SecretRepository]. */ + val secretRepository: SecretRepository = mockk() + + /** The [SecretService] used by the test factory. */ + val secretService = SecretService( + mockk(), + secretRepository, + SecretStorage(SecretsProviderFactoryForTesting().createProvider()) + ) /** The factory to be tested. */ - val factory: WorkerContextFactory = WorkerContextFactory(config, ortRunRepository, repositoryRepository) -) { + val factory: WorkerContextFactory = + WorkerContextFactory(config, ortRunRepository, repositoryRepository, secretService) + /** * Prepare the mock [OrtRunRepository] to be queried for the test run ID. Return a mock run that is also returned * by the repository. diff --git a/workers/config/build.gradle.kts b/workers/config/build.gradle.kts index f738bb249b..3bf07b3c40 100644 --- a/workers/config/build.gradle.kts +++ b/workers/config/build.gradle.kts @@ -65,6 +65,7 @@ dependencies { testImplementation(testFixtures(projects.config.configSpi)) testImplementation(testFixtures(projects.dao)) + testImplementation(testFixtures(projects.secrets.secretsSpi)) testImplementation(testFixtures(projects.transport.transportSpi)) testImplementation(libs.koinTest) diff --git a/workers/config/src/test/kotlin/EndpointTest.kt b/workers/config/src/test/kotlin/EndpointTest.kt index 03ccbb9c7a..b6ba07d839 100644 --- a/workers/config/src/test/kotlin/EndpointTest.kt +++ b/workers/config/src/test/kotlin/EndpointTest.kt @@ -35,6 +35,7 @@ import org.eclipse.apoapsis.ortserver.dao.test.withMockDatabaseModule import org.eclipse.apoapsis.ortserver.model.orchestrator.ConfigRequest import org.eclipse.apoapsis.ortserver.model.orchestrator.ConfigWorkerError import org.eclipse.apoapsis.ortserver.model.orchestrator.ConfigWorkerResult +import org.eclipse.apoapsis.ortserver.secrets.SecretsProviderFactoryForTesting import org.eclipse.apoapsis.ortserver.transport.ConfigEndpoint import org.eclipse.apoapsis.ortserver.transport.Message import org.eclipse.apoapsis.ortserver.transport.MessageHeader @@ -127,7 +128,8 @@ private suspend fun runEndpointTest(block: suspend () -> Unit) { val environment = mapOf( "CONFIG_RECEIVER_TRANSPORT_TYPE" to TEST_TRANSPORT_NAME, "ORCHESTRATOR_SENDER_TRANSPORT_TYPE" to TEST_TRANSPORT_NAME, - "CONFIG_SECRET_PROVIDER" to ConfigSecretProviderFactoryForTesting.NAME + "CONFIG_SECRET_PROVIDER" to ConfigSecretProviderFactoryForTesting.NAME, + "SECRETS_PROVIDER_NAME" to SecretsProviderFactoryForTesting.NAME ) withEnvironment(environment) { diff --git a/workers/evaluator/build.gradle.kts b/workers/evaluator/build.gradle.kts index ded8ce49a6..0d8c8f1277 100644 --- a/workers/evaluator/build.gradle.kts +++ b/workers/evaluator/build.gradle.kts @@ -67,6 +67,7 @@ dependencies { testImplementation(testFixtures(projects.config.configSpi)) testImplementation(testFixtures(projects.dao)) + testImplementation(testFixtures(projects.secrets.secretsSpi)) testImplementation(testFixtures(projects.transport.transportSpi)) testImplementation(libs.koinTest) diff --git a/workers/evaluator/src/test/kotlin/EvaluatorEndpointTest.kt b/workers/evaluator/src/test/kotlin/EvaluatorEndpointTest.kt index 1c0226591e..84795da025 100644 --- a/workers/evaluator/src/test/kotlin/EvaluatorEndpointTest.kt +++ b/workers/evaluator/src/test/kotlin/EvaluatorEndpointTest.kt @@ -37,6 +37,7 @@ import org.eclipse.apoapsis.ortserver.dao.test.withMockDatabaseModule import org.eclipse.apoapsis.ortserver.model.orchestrator.EvaluatorRequest import org.eclipse.apoapsis.ortserver.model.orchestrator.EvaluatorWorkerError import org.eclipse.apoapsis.ortserver.model.orchestrator.EvaluatorWorkerResult +import org.eclipse.apoapsis.ortserver.secrets.SecretsProviderFactoryForTesting import org.eclipse.apoapsis.ortserver.transport.EvaluatorEndpoint import org.eclipse.apoapsis.ortserver.transport.Message import org.eclipse.apoapsis.ortserver.transport.MessageHeader @@ -155,7 +156,8 @@ class EvaluatorEndpointTest : KoinTest, StringSpec() { val environment = mapOf( "EVALUATOR_RECEIVER_TRANSPORT_TYPE" to TEST_TRANSPORT_NAME, "ORCHESTRATOR_SENDER_TRANSPORT_TYPE" to TEST_TRANSPORT_NAME, - "EVALUATOR_SECRET_PROVIDER" to ConfigSecretProviderFactoryForTesting.NAME + "EVALUATOR_SECRET_PROVIDER" to ConfigSecretProviderFactoryForTesting.NAME, + "SECRETS_PROVIDER_NAME" to SecretsProviderFactoryForTesting.NAME ) withEnvironment(environment) { diff --git a/workers/notifier/build.gradle.kts b/workers/notifier/build.gradle.kts index db5c1c1b7f..e07fc85bf5 100644 --- a/workers/notifier/build.gradle.kts +++ b/workers/notifier/build.gradle.kts @@ -66,6 +66,7 @@ dependencies { testImplementation(libs.mockk) testImplementation(testFixtures(projects.config.configSpi)) testImplementation(testFixtures(projects.dao)) + testImplementation(testFixtures(projects.secrets.secretsSpi)) testImplementation(testFixtures(projects.transport.transportSpi)) } diff --git a/workers/notifier/src/test/kotlin/NotifierEndpointTest.kt b/workers/notifier/src/test/kotlin/NotifierEndpointTest.kt index d917af7ac5..c7e0aa6cd5 100644 --- a/workers/notifier/src/test/kotlin/NotifierEndpointTest.kt +++ b/workers/notifier/src/test/kotlin/NotifierEndpointTest.kt @@ -37,6 +37,7 @@ import org.eclipse.apoapsis.ortserver.dao.test.withMockDatabaseModule import org.eclipse.apoapsis.ortserver.model.orchestrator.NotifierRequest import org.eclipse.apoapsis.ortserver.model.orchestrator.NotifierWorkerError import org.eclipse.apoapsis.ortserver.model.orchestrator.NotifierWorkerResult +import org.eclipse.apoapsis.ortserver.secrets.SecretsProviderFactoryForTesting import org.eclipse.apoapsis.ortserver.transport.Message import org.eclipse.apoapsis.ortserver.transport.MessageHeader import org.eclipse.apoapsis.ortserver.transport.NotifierEndpoint @@ -134,7 +135,8 @@ class NotifierEndpointTest : KoinTest, StringSpec() { val environment = mapOf( "NOTIFIER_RECEIVER_TRANSPORT_TYPE" to TEST_TRANSPORT_NAME, "ORCHESTRATOR_SENDER_TRANSPORT_TYPE" to TEST_TRANSPORT_NAME, - "NOTIFIER_SECRET_PROVIDER" to ConfigSecretProviderFactoryForTesting.NAME + "NOTIFIER_SECRET_PROVIDER" to ConfigSecretProviderFactoryForTesting.NAME, + "SECRETS_PROVIDER_NAME" to SecretsProviderFactoryForTesting.NAME ) withEnvironment(environment) { diff --git a/workers/reporter/src/test/kotlin/reporter/ReporterRunnerTest.kt b/workers/reporter/src/test/kotlin/reporter/ReporterRunnerTest.kt index 9c357c48d4..541d672699 100644 --- a/workers/reporter/src/test/kotlin/reporter/ReporterRunnerTest.kt +++ b/workers/reporter/src/test/kotlin/reporter/ReporterRunnerTest.kt @@ -69,6 +69,9 @@ import org.eclipse.apoapsis.ortserver.model.OrtRunStatus import org.eclipse.apoapsis.ortserver.model.PluginConfig import org.eclipse.apoapsis.ortserver.model.ProviderPluginConfiguration import org.eclipse.apoapsis.ortserver.model.ReporterJobConfiguration +import org.eclipse.apoapsis.ortserver.model.ResolvablePluginConfig +import org.eclipse.apoapsis.ortserver.model.ResolvableSecret +import org.eclipse.apoapsis.ortserver.model.SecretSource import org.eclipse.apoapsis.ortserver.model.Severity import org.eclipse.apoapsis.ortserver.model.runs.repository.IssueResolution import org.eclipse.apoapsis.ortserver.model.runs.repository.Resolutions @@ -170,10 +173,10 @@ class ReporterRunnerTest : WordSpec({ every { createTempDir() } returnsMany listOf(outputDirectory, configDirectory) every { close() } just runs coEvery { resolvePluginConfigSecrets(any()) } answers { - val pluginConfigs: Map? = firstArg() + val pluginConfigs: Map? = firstArg() pluginConfigs.orEmpty().mapValues { entry -> val resolvedSecrets = entry.value.secrets.mapValues { secretEntry -> - "${secretEntry.value}_resolved" + "${secretEntry.value.name}_resolved" } PluginConfig(entry.value.options, resolvedSecrets) } @@ -242,8 +245,8 @@ class ReporterRunnerTest : WordSpec({ val jobConfig = ReporterJobConfiguration( formats = listOf(plainFormat, templateFormat), config = mapOf( - plainPluginId to PluginConfig(plainOptions, emptyMap()), - templatePluginId to PluginConfig( + plainPluginId to ResolvablePluginConfig(plainOptions, emptyMap()), + templatePluginId to ResolvablePluginConfig( mapOf( "ugly" to "false", "templateFile" to "${ReporterComponent.TEMPLATE_REFERENCE}$templateFileReference1", @@ -302,7 +305,7 @@ class ReporterRunnerTest : WordSpec({ val jobConfig = ReporterJobConfiguration( formats = listOf(templateFormat), config = mapOf( - templateFormat to PluginConfig( + templateFormat to ResolvablePluginConfig( mapOf( "templateFile" to "$otherReference1,${ReporterComponent.TEMPLATE_REFERENCE}$fileReference," + @@ -345,7 +348,7 @@ class ReporterRunnerTest : WordSpec({ val jobConfig = ReporterJobConfiguration( formats = listOf(templateFormat), config = mapOf( - templateFormat to PluginConfig( + templateFormat to ResolvablePluginConfig( mapOf("currentWorkingDir" to "${ReporterComponent.WORK_DIR_PLACEHOLDER}/reports"), emptyMap() ) @@ -376,12 +379,15 @@ class ReporterRunnerTest : WordSpec({ mockReporterFactoryAll(templateFormat to templateReporter) val options = mapOf("foo" to "bar") - val secrets = mapOf("username" to "secretUsername", "password" to "secretPassword") - val resolvedSecrets = secrets.mapValues { e -> e.value + "_resolved" } + val secrets = mapOf( + "username" to ResolvableSecret("secretUsername", SecretSource.ADMIN), + "password" to ResolvableSecret("secretPassword", SecretSource.ADMIN) + ) + val resolvedSecrets = secrets.mapValues { e -> e.value.name + "_resolved" } val jobConfig = ReporterJobConfiguration( formats = listOf(templateFormat), - config = mapOf(templateFormat to PluginConfig(options, secrets)) + config = mapOf(templateFormat to ResolvablePluginConfig(options, secrets)) ) val reporterConfig = createReporterConfig( @@ -995,14 +1001,14 @@ class ReporterRunnerTest : WordSpec({ val jobConfig = ReporterJobConfiguration( formats = listOf(templateFormat), config = mapOf( - templatePluginId to PluginConfig( + templatePluginId to ResolvablePluginConfig( mapOf( "ugly" to "false", "templateFile" to "someTemplate.ftl" ), emptyMap() ), - "$templatePluginId:$templateFormat" to PluginConfig( + "$templatePluginId:$templateFormat" to ResolvablePluginConfig( mapOf( "templateFile" to "anotherTemplate.ftl", "specialProperty" to "specialValue" diff --git a/workers/scanner/src/test/kotlin/ScannerRunnerTest.kt b/workers/scanner/src/test/kotlin/ScannerRunnerTest.kt index a74bcf5798..2a61546f11 100644 --- a/workers/scanner/src/test/kotlin/ScannerRunnerTest.kt +++ b/workers/scanner/src/test/kotlin/ScannerRunnerTest.kt @@ -35,7 +35,10 @@ import io.mockk.verify import org.eclipse.apoapsis.ortserver.config.Context import org.eclipse.apoapsis.ortserver.model.PluginConfig +import org.eclipse.apoapsis.ortserver.model.ResolvablePluginConfig +import org.eclipse.apoapsis.ortserver.model.ResolvableSecret import org.eclipse.apoapsis.ortserver.model.ScannerJobConfiguration +import org.eclipse.apoapsis.ortserver.model.SecretSource import org.eclipse.apoapsis.ortserver.model.SourceCodeOrigin import org.eclipse.apoapsis.ortserver.model.SubmoduleFetchStrategy import org.eclipse.apoapsis.ortserver.services.config.AdminConfig @@ -115,16 +118,22 @@ class ScannerRunnerTest : WordSpec({ val licenseeFactory = mockScannerWrapperFactory("Licensee") mockScannerWrapperAll(listOf(scanCodeFactory, licenseeFactory)) - val scanCodeSecretRefs = mapOf("secret1" to "passRef1", "secret2" to "passRef2") + val scanCodeSecretRefs = mapOf( + "secret1" to ResolvableSecret("passRef1", SecretSource.ADMIN), + "secret2" to ResolvableSecret("passRef2", SecretSource.ADMIN) + ) val scanCodeSecrets = mapOf("secret1" to "pass1", "secret2" to "pass2") - val scanCodeConfig = PluginConfig( + val scanCodeConfig = ResolvablePluginConfig( options = mapOf("option1" to "value1", "option2" to "value2"), secrets = scanCodeSecretRefs ) - val licenseeSecretRefs = mapOf("secret3" to "passRef3", "secret4" to "passRef4") + val licenseeSecretRefs = mapOf( + "secret3" to ResolvableSecret("passRef3", SecretSource.ADMIN), + "secret4" to ResolvableSecret("passRef4", SecretSource.ADMIN) + ) val licenseeSecrets = mapOf("secret3" to "pass3", "secret4" to "pass4") - val licenseeConfig = PluginConfig( + val licenseeConfig = ResolvablePluginConfig( options = mapOf("option3" to "value3", "option4" to "value4"), secrets = licenseeSecretRefs ) @@ -139,15 +148,19 @@ class ScannerRunnerTest : WordSpec({ ) val resolvedPluginConfig = mapOf( - "ScanCode" to scanCodeConfig.copy(secrets = scanCodeSecrets), - "Licensee" to licenseeConfig.copy(secrets = licenseeSecrets) + "ScanCode" to PluginConfig(options = scanCodeConfig.options, secrets = scanCodeSecrets), + "Licensee" to PluginConfig(options = licenseeConfig.options, secrets = licenseeSecrets) ) val context = mockContext(jobConfig, resolvedPluginConfig) runner.run(context, OrtResult.EMPTY, jobConfig, 0L) verify(exactly = 1) { - scanCodeFactory.create(scanCodeConfig.copy(secrets = scanCodeSecrets).mapToOrt()) - licenseeFactory.create(licenseeConfig.copy(secrets = licenseeSecrets).mapToOrt()) + scanCodeFactory.create( + PluginConfig(options = scanCodeConfig.options, secrets = scanCodeSecrets).mapToOrt() + ) + licenseeFactory.create( + PluginConfig(options = licenseeConfig.options, secrets = licenseeSecrets).mapToOrt() + ) } }