diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/configuration/application/ProvWithSudo.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/configuration/application/ProvWithSudo.kt new file mode 100644 index 0000000..15a171e --- /dev/null +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/configuration/application/ProvWithSudo.kt @@ -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 + } + + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/application/Application.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/application/Application.kt index 5f9fef5..b9d37ee 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/application/Application.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/application/Application.kt @@ -1,6 +1,7 @@ package org.domaindrivenarchitecture.provs.desktop.application import kotlinx.serialization.SerializationException +import org.domaindrivenarchitecture.provs.configuration.application.ensureSudoWithoutPassword import org.domaindrivenarchitecture.provs.desktop.domain.provisionDesktopCommand import org.domaindrivenarchitecture.provs.framework.core.cli.createProvInstance import java.io.FileNotFoundException @@ -18,9 +19,10 @@ fun main(args: Array) { } val prov = createProvInstance(cmd.target) + val provWithSudo = ensureSudoWithoutPassword(prov, cmd.target) try { - provisionDesktopCommand(prov, cmd) + provisionDesktopCommand(provWithSudo, cmd) } catch (e: SerializationException) { println( "Error: File \"${cmd.configFile?.fileName}\" has an invalid format and or invalid data.\n" diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/cli/CliUtils.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/cli/CliUtils.kt index 0107ea6..bd01c07 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/cli/CliUtils.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/cli/CliUtils.kt @@ -6,9 +6,6 @@ import org.domaindrivenarchitecture.provs.framework.core.Secret import org.domaindrivenarchitecture.provs.framework.core.local import org.domaindrivenarchitecture.provs.framework.core.remote 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 @@ -21,17 +18,10 @@ fun createProvInstance(targetCommand: TargetCliCommand): Prov { if (targetCommand.isValid()) { val password: Secret? = targetCommand.remoteTarget()?.password - val remoteTarget = targetCommand.remoteTarget() - return if (targetCommand.isValidLocalhost()) { - createLocalProvInstance() - } else if (targetCommand.isValidRemote() && remoteTarget != null) { - createRemoteProvInstance( - remoteTarget.host, - remoteTarget.user, - remoteTarget.password == null, - password - ) + local() + } else if (targetCommand.isValidRemote()) { + createRemoteProvInstance(targetCommand.remoteTarget(), password) } else { throw IllegalArgumentException( "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) - - check(prov.currentUserCanSudoWithoutPassword()) { - "ERROR: User ${prov.whoami()} cannot sudo without enteringa password." - } - } - return prov -} - - -private fun createRemoteProvInstance( - host: String, - remoteUser: String, - sshWithKey: Boolean, - password: Secret? +internal fun createRemoteProvInstance( + target: TargetCliCommand.RemoteTarget?, + password: Secret? = null ): Prov { - val prov = - if (sshWithKey) { - remote(host, remoteUser) - } else { - require(password != null) { - "No password available for provisioning without ssh keys. " + - "Either specify provisioning by ssh-keys or provide password." - } - remote(host, remoteUser, password) - } - - return if (prov.currentUserCanSudoWithoutPassword()) { - prov + return if (target != null) { + remote(target.host, target.user, target.password ?: password) } 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 + throw IllegalArgumentException( + "Error: no valid remote target (host & user) was specified!" + ) } } + internal fun getPasswordToConfigureSudoWithoutPassword(): Secret { return PromptSecretSource("password to configure sudo without password.").secret() } \ No newline at end of file diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/processors/ContainerUbuntuHostProcessor.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/processors/ContainerUbuntuHostProcessor.kt index 9c8a880..8391e97 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/processors/ContainerUbuntuHostProcessor.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/processors/ContainerUbuntuHostProcessor.kt @@ -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.escapeAndEncloseByDoubleQuoteForShell import org.domaindrivenarchitecture.provs.framework.core.platforms.SHELL -import org.domaindrivenarchitecture.provs.framework.core.tags.Api enum class ContainerStartMode { USE_RUNNING_ELSE_CREATE, @@ -20,26 +19,24 @@ enum class ContainerEndMode { open class ContainerUbuntuHostProcessor( private val containerName: String = "default_provs_container", - @Api // suppress false positive warning - private val dockerImage: String = "ubuntu", - @Api // suppress false positive warning - private val startMode: ContainerStartMode = ContainerStartMode.USE_RUNNING_ELSE_CREATE, + dockerImage: String = "ubuntu", + startMode: ContainerStartMode = ContainerStartMode.USE_RUNNING_ELSE_CREATE, private val endMode: ContainerEndMode = ContainerEndMode.KEEP_RUNNING, - @Api // suppress false positive warning - private val sudo: Boolean = true + sudo: Boolean = true, + options: String = "" ) : Processor { + + private val hostShell = "/bin/bash" private val dockerCmd = if (sudo) "sudo docker " else "docker " private var localExecution = LocalProcessor() private var a = Prov.newInstance(name = "LocalProcessor for Docker operations", progressType = ProgressType.NONE) init { - val r = a.provideContainer(containerName, dockerImage, startMode, sudo) - if (!r.success) - throw RuntimeException("Could not start docker image: " + r.toString(), r.exception) + val result = a.provideContainer(containerName, dockerImage, startMode, sudo, options) + if (!result.success) + throw RuntimeException("Could not start docker image: " + result.toString(), result.exception) } - private val hostShell = "/bin/bash" - override fun exec(vararg args: String): ProcessResult { return localExecution.exec(hostShell, "-c", dockerCmd + "exec $containerName " + buildCommand(*args)) } @@ -57,7 +54,7 @@ open class ContainerUbuntuHostProcessor( return s.escapeAndEncloseByDoubleQuoteForShell() } - private fun buildCommand(vararg args: String) : String { + private fun buildCommand(vararg args: String): String { return if (args.size == 1) quoteString(args[0]) else if (args.size == 3 && SHELL == args[0] && "-c" == args[1]) SHELL + " -c " + quoteString(args[2]) else args.joinToString(separator = " ") diff --git a/src/test/kotlin/org/domaindrivenarchitecture/provs/configuration/application/ProvWithSudoKtTest.kt b/src/test/kotlin/org/domaindrivenarchitecture/provs/configuration/application/ProvWithSudoKtTest.kt new file mode 100644 index 0000000..29c3670 --- /dev/null +++ b/src/test/kotlin/org/domaindrivenarchitecture/provs/configuration/application/ProvWithSudoKtTest.kt @@ -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) + } +} \ No newline at end of file