[skip ci] refactor ensureSudoWithoutPassword to application layer

This commit is contained in:
az 2023-03-20 18:22:47 +01:00
parent 8a4bbe97f9
commit 1497d390f6
5 changed files with 159 additions and 77 deletions

View file

@ -0,0 +1,32 @@
package org.domaindrivenarchitecture.provs.configuration.application
import org.domaindrivenarchitecture.provs.configuration.domain.TargetCliCommand
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.core.cli.createRemoteProvInstance
import org.domaindrivenarchitecture.provs.framework.core.cli.getPasswordToConfigureSudoWithoutPassword
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.currentUserCanSudoWithoutPassword
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.makeCurrentUserSudoerWithoutPasswordRequired
fun ensureSudoWithoutPassword(prov: Prov, targetCommand: TargetCliCommand): Prov {
return if (prov.currentUserCanSudoWithoutPassword()) {
prov
} else {
val password = targetCommand.remoteTarget()?.password ?: getPasswordToConfigureSudoWithoutPassword()
val result = prov.makeCurrentUserSudoerWithoutPasswordRequired(password)
check(result.success) {
"Could not make user a sudoer without password required. (E.g. the password provided may be incorrect.)"
}
return if (targetCommand.isValidRemote()) {
// return a new instance as for remote instances a new ssh client is required after user was made sudoer without password
createRemoteProvInstance(targetCommand.remoteTarget())
} else {
prov
}
}
}

View file

@ -1,6 +1,7 @@
package org.domaindrivenarchitecture.provs.desktop.application package org.domaindrivenarchitecture.provs.desktop.application
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
import org.domaindrivenarchitecture.provs.configuration.application.ensureSudoWithoutPassword
import org.domaindrivenarchitecture.provs.desktop.domain.provisionDesktopCommand import org.domaindrivenarchitecture.provs.desktop.domain.provisionDesktopCommand
import org.domaindrivenarchitecture.provs.framework.core.cli.createProvInstance import org.domaindrivenarchitecture.provs.framework.core.cli.createProvInstance
import java.io.FileNotFoundException import java.io.FileNotFoundException
@ -18,9 +19,10 @@ fun main(args: Array<String>) {
} }
val prov = createProvInstance(cmd.target) val prov = createProvInstance(cmd.target)
val provWithSudo = ensureSudoWithoutPassword(prov, cmd.target)
try { try {
provisionDesktopCommand(prov, cmd) provisionDesktopCommand(provWithSudo, cmd)
} catch (e: SerializationException) { } catch (e: SerializationException) {
println( println(
"Error: File \"${cmd.configFile?.fileName}\" has an invalid format and or invalid data.\n" "Error: File \"${cmd.configFile?.fileName}\" has an invalid format and or invalid data.\n"

View file

@ -6,9 +6,6 @@ import org.domaindrivenarchitecture.provs.framework.core.Secret
import org.domaindrivenarchitecture.provs.framework.core.local import org.domaindrivenarchitecture.provs.framework.core.local
import org.domaindrivenarchitecture.provs.framework.core.remote import org.domaindrivenarchitecture.provs.framework.core.remote
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources.PromptSecretSource import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources.PromptSecretSource
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.currentUserCanSudoWithoutPassword
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.makeCurrentUserSudoerWithoutPasswordRequired
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.whoami
import kotlin.system.exitProcess import kotlin.system.exitProcess
@ -21,17 +18,10 @@ fun createProvInstance(targetCommand: TargetCliCommand): Prov {
if (targetCommand.isValid()) { if (targetCommand.isValid()) {
val password: Secret? = targetCommand.remoteTarget()?.password val password: Secret? = targetCommand.remoteTarget()?.password
val remoteTarget = targetCommand.remoteTarget()
return if (targetCommand.isValidLocalhost()) { return if (targetCommand.isValidLocalhost()) {
createLocalProvInstance() local()
} else if (targetCommand.isValidRemote() && remoteTarget != null) { } else if (targetCommand.isValidRemote()) {
createRemoteProvInstance( createRemoteProvInstance(targetCommand.remoteTarget(), password)
remoteTarget.host,
remoteTarget.user,
remoteTarget.password == null,
password
)
} else { } else {
throw IllegalArgumentException( throw IllegalArgumentException(
"Error: neither a valid localHost nor a valid remoteHost was specified! Use option -h for help." "Error: neither a valid localHost nor a valid remoteHost was specified! Use option -h for help."
@ -44,62 +34,21 @@ fun createProvInstance(targetCommand: TargetCliCommand): Prov {
} }
} }
private fun createLocalProvInstance(): Prov {
val prov = local()
if (!prov.currentUserCanSudoWithoutPassword()) {
val passwordNonNull = getPasswordToConfigureSudoWithoutPassword()
prov.makeCurrentUserSudoerWithoutPasswordRequired(passwordNonNull) internal fun createRemoteProvInstance(
target: TargetCliCommand.RemoteTarget?,
check(prov.currentUserCanSudoWithoutPassword()) { password: Secret? = null
"ERROR: User ${prov.whoami()} cannot sudo without enteringa password."
}
}
return prov
}
private fun createRemoteProvInstance(
host: String,
remoteUser: String,
sshWithKey: Boolean,
password: Secret?
): Prov { ): Prov {
val prov = return if (target != null) {
if (sshWithKey) { remote(target.host, target.user, target.password ?: password)
remote(host, remoteUser)
} else { } else {
require(password != null) { throw IllegalArgumentException(
"No password available for provisioning without ssh keys. " + "Error: no valid remote target (host & user) was specified!"
"Either specify provisioning by ssh-keys or provide password." )
}
remote(host, remoteUser, password)
}
return if (prov.currentUserCanSudoWithoutPassword()) {
prov
} else {
val passwordNonNull = password
?: getPasswordToConfigureSudoWithoutPassword()
val result = prov.makeCurrentUserSudoerWithoutPasswordRequired(passwordNonNull)
check(result.success) {
"Could not make user a sudoer without password required. (Maybe the provided password is incorrect.)"
}
// a new session is required after the user has become a sudoer without password
val provWithNewSshClient = remote(host, remoteUser, password)
check(provWithNewSshClient.currentUserCanSudoWithoutPassword()) {
"ERROR: User ${provWithNewSshClient.whoami()} on $host cannot sudo without entering a password."
}
provWithNewSshClient
} }
} }
internal fun getPasswordToConfigureSudoWithoutPassword(): Secret { internal fun getPasswordToConfigureSudoWithoutPassword(): Secret {
return PromptSecretSource("password to configure sudo without password.").secret() return PromptSecretSource("password to configure sudo without password.").secret()
} }

View file

@ -5,7 +5,6 @@ import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.core.docker.provideContainer import org.domaindrivenarchitecture.provs.framework.core.docker.provideContainer
import org.domaindrivenarchitecture.provs.framework.core.escapeAndEncloseByDoubleQuoteForShell import org.domaindrivenarchitecture.provs.framework.core.escapeAndEncloseByDoubleQuoteForShell
import org.domaindrivenarchitecture.provs.framework.core.platforms.SHELL import org.domaindrivenarchitecture.provs.framework.core.platforms.SHELL
import org.domaindrivenarchitecture.provs.framework.core.tags.Api
enum class ContainerStartMode { enum class ContainerStartMode {
USE_RUNNING_ELSE_CREATE, USE_RUNNING_ELSE_CREATE,
@ -20,26 +19,24 @@ enum class ContainerEndMode {
open class ContainerUbuntuHostProcessor( open class ContainerUbuntuHostProcessor(
private val containerName: String = "default_provs_container", private val containerName: String = "default_provs_container",
@Api // suppress false positive warning dockerImage: String = "ubuntu",
private val dockerImage: String = "ubuntu", startMode: ContainerStartMode = ContainerStartMode.USE_RUNNING_ELSE_CREATE,
@Api // suppress false positive warning
private val startMode: ContainerStartMode = ContainerStartMode.USE_RUNNING_ELSE_CREATE,
private val endMode: ContainerEndMode = ContainerEndMode.KEEP_RUNNING, private val endMode: ContainerEndMode = ContainerEndMode.KEEP_RUNNING,
@Api // suppress false positive warning sudo: Boolean = true,
private val sudo: Boolean = true options: String = ""
) : Processor { ) : Processor {
private val hostShell = "/bin/bash"
private val dockerCmd = if (sudo) "sudo docker " else "docker " private val dockerCmd = if (sudo) "sudo docker " else "docker "
private var localExecution = LocalProcessor() private var localExecution = LocalProcessor()
private var a = Prov.newInstance(name = "LocalProcessor for Docker operations", progressType = ProgressType.NONE) private var a = Prov.newInstance(name = "LocalProcessor for Docker operations", progressType = ProgressType.NONE)
init { init {
val r = a.provideContainer(containerName, dockerImage, startMode, sudo) val result = a.provideContainer(containerName, dockerImage, startMode, sudo, options)
if (!r.success) if (!result.success)
throw RuntimeException("Could not start docker image: " + r.toString(), r.exception) throw RuntimeException("Could not start docker image: " + result.toString(), result.exception)
} }
private val hostShell = "/bin/bash"
override fun exec(vararg args: String): ProcessResult { override fun exec(vararg args: String): ProcessResult {
return localExecution.exec(hostShell, "-c", dockerCmd + "exec $containerName " + buildCommand(*args)) return localExecution.exec(hostShell, "-c", dockerCmd + "exec $containerName " + buildCommand(*args))
} }

View file

@ -0,0 +1,102 @@
package org.domaindrivenarchitecture.provs.configuration.application
import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import org.domaindrivenarchitecture.provs.configuration.domain.TargetCliCommand
import org.domaindrivenarchitecture.provs.framework.core.*
import org.domaindrivenarchitecture.provs.framework.core.cli.getPasswordToConfigureSudoWithoutPassword
import org.domaindrivenarchitecture.provs.framework.core.docker.provideContainer
import org.domaindrivenarchitecture.provs.framework.core.processors.ContainerStartMode
import org.domaindrivenarchitecture.provs.framework.core.processors.ContainerUbuntuHostProcessor
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.deleteFile
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstall
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.currentUserCanSudoWithoutPassword
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.makeCurrentUserSudoerWithoutPasswordRequired
import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
class ProvWithSudoKtTest {
@ExtensiveContainerTest
fun test_ensureSudoWithoutPassword_local_Prov() {
mockkStatic(::getPasswordToConfigureSudoWithoutPassword)
every { getPasswordToConfigureSudoWithoutPassword() } returns Secret("testuserpw")
// given
val containerName = "prov-test-sudo-no-pw"
local().provideContainer(containerName, "ubuntu_plus_user")
val prov = Prov.newInstance(
ContainerUbuntuHostProcessor(
containerName,
startMode = ContainerStartMode.USE_RUNNING_ELSE_CREATE,
sudo = true,
dockerImage = "ubuntu_plus_user"
),
progressType = ProgressType.NONE
)
prov.deleteFile("/etc/sudoers.d/testuser", sudo = true) // remove no password required config
// when
val canSudo1 = prov.currentUserCanSudoWithoutPassword()
val provWithSudo = ensureSudoWithoutPassword(
prov, TargetCliCommand("local")
)
val canSudo2 = provWithSudo.currentUserCanSudoWithoutPassword()
// then
assertFalse(canSudo1)
assertTrue(canSudo2)
unmockkStatic(::getPasswordToConfigureSudoWithoutPassword)
}
@ExtensiveContainerTest
fun test_ensureSudoWithoutPassword_remote_Prov() {
// mockkStatic(::getPasswordToConfigureSudoWithoutPassword)
// every { getPasswordToConfigureSudoWithoutPassword() } returns Secret("testuserpw")
// given
val containerName = "prov-test-sudo-no-pw-ssh"
val password = Secret("testuserpw")
// local().provideContainer(containerName, "ubuntu_plus_user", options = "")
val prov = Prov.newInstance(
ContainerUbuntuHostProcessor(
containerName,
startMode = ContainerStartMode.USE_RUNNING_ELSE_CREATE,
sudo = true,
dockerImage = "ubuntu_plus_user",
options = "--expose=22"
),
progressType = ProgressType.NONE
)
prov.makeCurrentUserSudoerWithoutPasswordRequired(password)
prov.task {
aptInstall("openssh-server")
cmd("sudo service ssh start")
deleteFile("/etc/sudoers.d/testuser", sudo = true) // remove no password required config
}
val ip = local().cmd("sudo docker inspect -f \"{{ .NetworkSettings.IPAddress }}\" $containerName").out?.trim()
?: throw IllegalStateException("Ip not found")
val remoteProvBySsh = remote(ip, "testuser", password)
// when
val canSudo1 = remoteProvBySsh.currentUserCanSudoWithoutPassword()
val provWithSudo = ensureSudoWithoutPassword(
remoteProvBySsh, TargetCliCommand("testuser:${password.plain()}@$ip")
)
val canSudo2 = provWithSudo.currentUserCanSudoWithoutPassword()
// then
assertFalse(canSudo1)
assertTrue(canSudo2)
// unmockkStatic(::getPasswordToConfigureSudoWithoutPassword)
}
}