Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion model/src/commonMain/kotlin/Hierarchy.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,12 @@ data class Hierarchy(

/** The [Organization] the current repository and product belong to. */
val organization: Organization
)
) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from config secrets

Should this say "from admin secrets"?

val compoundId: CompoundHierarchyId by lazy {
CompoundHierarchyId.forRepository(
organizationId = OrganizationId(organization.id),
productId = ProductId(product.id),
repositoryId = RepositoryId(repository.id)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@ 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.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
Expand Down Expand Up @@ -113,6 +115,10 @@ internal class WorkerContextImpl(
repositoryRepository.getHierarchy(ortRun.repositoryId)
}

private val hierarchySecrets by lazy {
runBlocking { secretService.listForHierarchy(hierarchy) }.associateBy { it.name }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about this usage of runBlocking here, as it blocks the calling thread. It would probably be more coroutine-friendly to manage the field manually using a Mutex instead of the lazy approach.
Or, if the field is only used to test whether a referenced secret exists, could this test be shifted to later when the secret value is actually resolved?

}

override val credentialResolverFun: CredentialResolverFun
get() = { secret ->
refCredentialResolverFun.get().invoke(secret)
Expand All @@ -134,10 +140,22 @@ internal class WorkerContextImpl(
config: Map<String, ResolvablePluginConfig>?
): Map<String, PluginConfig> =
config?.let { c ->
val secrets = c.values.flatMap { pluginConfig -> pluginConfig.secrets.values.map { it.name } }
val resolvedSecrets = parallelTransform(secrets, configSecretsCache, this::resolveConfigSecret) { it }
val configSecrets = c.values.flatMap { pluginConfig ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use partition here to distinguish between config and user secrets (assuming that there are only two sources)?

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}'.")
}
}

c.mapValues { (_, pluginConfig) -> pluginConfig.resolveSecrets(resolvedSecrets) }
val resolvedConfigSecrets =
parallelTransform(configSecrets, configSecretsCache, ::resolveConfigSecret) { it }
val resolvedUserSecrets = parallelTransform(userSecrets, secretsCache, ::resolveSecretValue) { it }
.mapKeys { it.key.name }

c.mapValues { (_, pluginConfig) -> pluginConfig.resolveSecrets(resolvedConfigSecrets, resolvedUserSecrets) }
}.orEmpty()

override suspend fun resolveProviderPluginConfigSecrets(
Expand Down Expand Up @@ -293,10 +311,20 @@ 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 ResolvablePluginConfig.resolveSecrets(secretValues: Map<String, String>): PluginConfig {
val resolvedSecrets = secrets.mapValues { e -> secretValues.getValue(e.value.name) }
private fun ResolvablePluginConfig.resolveSecrets(
configSecretValues: Map<String, String>,
userSecretValues: Map<String, String>
): 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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,28 @@ 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
Expand Down Expand Up @@ -327,6 +337,63 @@ class WorkerContextTest : WordSpec({
}

"return plugin configurations with resolved secrets" {
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(
Expand All @@ -337,8 +404,8 @@ class WorkerContextTest : WordSpec({
val pluginConfig2 = ResolvablePluginConfig(
options = mapOf("plugin2Option" to "v3"),
secrets = mapOf(
"plugin2ServiceUser" to ResolvableSecret("serviceUser", SecretSource.ADMIN),
"plugin2ServicePassword" to ResolvableSecret("servicePassword", SecretSource.ADMIN),
"plugin2ServiceUser" to ResolvableSecret("serviceUser", SecretSource.USER),
"plugin2ServicePassword" to ResolvableSecret("servicePassword", SecretSource.USER),
"plugin2DBAccess" to ResolvableSecret("dbPassword", SecretSource.ADMIN)
)
)
Expand All @@ -358,7 +425,9 @@ class WorkerContextTest : WordSpec({
)
val expectedConfig = mapOf("p1" to resolvedConfig1, "p2" to resolvedConfig2)

val resolvedConfig = helper.context().resolvePluginConfigSecrets(config)
val resolvedConfig = mockkTransaction {
helper.context().resolvePluginConfigSecrets(config)
}

resolvedConfig shouldBe expectedConfig
}
Expand Down
Loading