diff --git a/doc/CreateProvInstanceSequence.md b/doc/CreateProvInstanceSequence.md new file mode 100644 index 0000000..c3e18aa --- /dev/null +++ b/doc/CreateProvInstanceSequence.md @@ -0,0 +1,55 @@ +```plantuml +@startuml + +autonumber + +skinparam sequenceBox { + borderColor White +} + +participant Cli +participant Application +participant CliArgumentsParser +participant CliTargetCommand +participant CliUtils +participant "CliUtils\ncreateLocalProv" as CliUtilsL +participant "CliUtils\ncreateRemoteProv" as CliUtilsR +participant Prov +participant PromptSecretSource +participant User + +Cli -> Application ++ : main(args...) +Application -> CliArgumentsParser : parseCommand + +CliArgumentsParser -> CliTargetCommand : create() +Application -> CliUtils : createProvInstance( targetCliCommand ) +alt target.isValidLocal +CliUtils -> CliUtilsL : createLocalProv +CliUtilsL -> Prov : createLocalInstance +alt userCannotSudoWithoutPw +CliUtilsL -> PromptSecretSource : getPassword +CliUtilsL -> User : makeUserSudoWithoutPw +CliUtilsL --> CliUtils : provInstance +CliUtils --> Application : provInstance +end +else target.isValidRemote +CliUtils -> CliUtilsR : createRemoteProv +CliUtilsR -> Prov : createRemoteInstance +alt userCannotSudoWithoutPw +CliUtilsR -> PromptSecretSource : getPassword +CliUtilsR -> User : makeUserSudoWithoutPw +CliUtilsR -> Prov : createRemoteInstance\n[new ssh-client is required] +CliUtilsR --> CliUtils : provInstance +CliUtils --> Application : provInstance +end +end + +Application -> DesktopService1 : provisionDesktopCommand ( provInstance, desktopCliCommand ) + + +'DesktopService1 -> DesktopService2 : provisionDesktop( config ) +'DesktopService1 -> ConfigRepository : getConfig + +@enduml + +``` \ No newline at end of file diff --git a/doc/DesktopCliParsingSequence.md b/doc/DesktopCliParsingSequence.md index 5ba2960..025426e 100644 --- a/doc/DesktopCliParsingSequence.md +++ b/doc/DesktopCliParsingSequence.md @@ -24,7 +24,6 @@ CliArgumentsParser -> DesktopCliCommand : create(desktopType, cliTargetCmd, ...) CliArgumentsParser --> Application: desktopCliCommand Application -> DesktopCliCommand : isValid ? Application -> CliUtils : createProvInstance -ProvInstance <- CliUtils : create alt target.isValidLocal CliUtils -> CliUtils : createLocalProv else target.isValidRemote 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 780bd37..5f9fef5 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/application/Application.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/application/Application.kt @@ -17,7 +17,7 @@ fun main(args: Array) { exitProcess(1) } - val prov = createProvInstance(cmd.target, remoteHostSetSudoWithoutPasswordRequired = true) + val prov = createProvInstance(cmd.target) try { provisionDesktopCommand(prov, cmd) 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 7e3055f..0107ea6 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 @@ -7,42 +7,39 @@ 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.makeUserSudoerWithNoSudoPasswordRequired +import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.makeCurrentUserSudoerWithoutPasswordRequired import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.whoami import kotlin.system.exitProcess /** * Returns a Prov instance according to the targetCommand. - * E.g. it returns a local Prov instance if targetCommand.isValidLocalhost() is true or + * Returns a local Prov instance if targetCommand.isValidLocalhost() is true resp. * returns a remote Prov instance if targetCommand.isValidRemote() is true. - * - * If the target is remote and if parameter remoteHostSetSudoWithoutPasswordRequired is set to true, - * it will enable sudo without password on the remote machine (in case this was not yet enabled). */ -fun createProvInstance( - targetCommand: TargetCliCommand, - remoteHostSetSudoWithoutPasswordRequired: Boolean = false -): Prov { +fun createProvInstance(targetCommand: TargetCliCommand): Prov { if (targetCommand.isValid()) { val password: Secret? = targetCommand.remoteTarget()?.password val remoteTarget = targetCommand.remoteTarget() - if (targetCommand.isValidLocalhost()) { - return createLocalProvInstance() + + return if (targetCommand.isValidLocalhost()) { + createLocalProvInstance() } else if (targetCommand.isValidRemote() && remoteTarget != null) { - return createRemoteProvInstance( + createRemoteProvInstance( remoteTarget.host, remoteTarget.user, remoteTarget.password == null, - password, - remoteHostSetSudoWithoutPasswordRequired + password ) } else { - throw IllegalArgumentException("Error: neither a valid localHost nor a valid remoteHost was specified! Use option -h for help.") + throw IllegalArgumentException( + "Error: neither a valid localHost nor a valid remoteHost was specified! Use option -h for help." + ) } } else { - println("Invalid command line options.\nPlease use option -h for help.") + println("ERROR: Invalid target (${targetCommand.target}). Please use option -h for help.") + System.out.flush() exitProcess(1) } } @@ -50,9 +47,13 @@ fun createProvInstance( private fun createLocalProvInstance(): Prov { val prov = local() if (!prov.currentUserCanSudoWithoutPassword()) { - val password = PromptSecretSource("Please enter password to configure sudo without password in the future." + - "\nWarning: This will permanently allow your user to use sudo privileges without a password.").secret() - prov.makeUserSudoerWithNoSudoPasswordRequired(password) + val passwordNonNull = getPasswordToConfigureSudoWithoutPassword() + + prov.makeCurrentUserSudoerWithoutPasswordRequired(passwordNonNull) + + check(prov.currentUserCanSudoWithoutPassword()) { + "ERROR: User ${prov.whoami()} cannot sudo without enteringa password." + } } return prov } @@ -62,42 +63,43 @@ private fun createRemoteProvInstance( host: String, remoteUser: String, sshWithKey: Boolean, - password: Secret?, - remoteHostSetSudoWithoutPasswordRequired: Boolean + password: Secret? ): 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." }) + require(password != null) { + "No password available for provisioning without ssh keys. " + + "Either specify provisioning by ssh-keys or provide password." + } remote(host, remoteUser, password) } - if (!prov.currentUserCanSudoWithoutPassword()) { - if (remoteHostSetSudoWithoutPasswordRequired) { - require( - password != null, - { "User ${prov.whoami()} not able to sudo on remote machine without password and no password available for the user." }) - prov.makeUserSudoerWithNoSudoPasswordRequired(password) + return if (prov.currentUserCanSudoWithoutPassword()) { + prov + } else { - // a new session is required after making the user a sudoer without password - return remote(host, remoteUser, password) - } else { - throw IllegalStateException("User ${prov.whoami()} not able to sudo on remote machine without password and option not set to enable user to sudo without password.") + 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 } - return prov } - -internal fun retrievePassword(cliCommand: TargetCliCommand): Secret? { - var password: Secret? = null - if (cliCommand.isValidRemote() && cliCommand.passwordInteractive) { - password = - PromptSecretSource("Password for user $cliCommand.userName!! on $cliCommand.remoteHost!!").secret() - - } - return password -} +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/platforms/UbuntuProv.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/platforms/UbuntuProv.kt index 254e333..c85db20 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/platforms/UbuntuProv.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/platforms/UbuntuProv.kt @@ -13,13 +13,6 @@ class UbuntuProv internal constructor( progressType: ProgressType ) : Prov(processor, name, progressType) { - init { - val user = cmdNoLog("whoami").out?.trim() - if ("root" != user && !cmdNoLog("timeout 1 sudo id").success) { - println("IMPORTANT INFO:\nUser $user cannot sudo without entering a password, i.e. some functions may fail!\nIf you need to run functions with sudo, please ensure $user can sudo without password.") - } - } - override fun cmd(cmd: String, dir: String?, sudo: Boolean): ProvResult = taskWithResult { exec(SHELL, "-c", commandWithDirAndSudo(cmd, dir, sudo)) } diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/processors/RemoteUbuntuProcessor.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/processors/RemoteUbuntuProcessor.kt index f6e08ff..ead6482 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/processors/RemoteUbuntuProcessor.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/processors/RemoteUbuntuProcessor.kt @@ -38,6 +38,8 @@ class RemoteProcessor(host: InetAddress, user: String, password: Secret? = null) // Attention: host key is not verified ssh.addHostKeyVerifier(PromiscuousVerifier()) + + ssh.connectTimeout = 30000 // ms ssh.connect(host) if (password != null) { @@ -50,8 +52,9 @@ class RemoteProcessor(host: InetAddress, user: String, password: Secret? = null) try { ssh.disconnect() } finally { - log.error("Got exception when initializing ssh (Username, password or ssh-key might be wrong): " + e.message) - throw RuntimeException("Error when initializing ssh (Username, password or ssh-key might be wrong) ", e) + val errorMag = "Error when initializing ssh (Host, username, password or ssh-key might be wrong) " + log.error(errorMag + e.message) + throw RuntimeException(errorMag, e) } } } diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/secret/secretSources/PromptSecretSource.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/secret/secretSources/PromptSecretSource.kt index bbb9919..bf69f2f 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/secret/secretSources/PromptSecretSource.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/secret/secretSources/PromptSecretSource.kt @@ -8,7 +8,7 @@ import javax.swing.* class PasswordPanel : JPanel(FlowLayout()) { - private val passwordField = JPasswordField(20) + private val passwordField = JPasswordField(30) private var entered = false val enteredPassword diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/user/base/User.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/user/base/User.kt index 5da27f4..1405542 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/user/base/User.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/ubuntu/user/base/User.kt @@ -33,7 +33,7 @@ fun Prov.createUser( } password?.let { cmdNoLog("sudo echo \"$userName:${password.plain()}\" | sudo chpasswd") } ?: ProvResult(true) if (userCanSudoWithoutPassword) { - makeUserSudoerWithNoSudoPasswordRequired(userName) + makeUserSudoerWithoutPasswordRequired(userName) } val authorizedKeysFile = userHome() + ".ssh/authorized_keys" if (copyAuthorizedSshKeysFromCurrentUser && checkFile(authorizedKeysFile)) { @@ -85,11 +85,11 @@ fun Prov.deleteUser(userName: String, deleteHomeDir: Boolean = false): ProvResul * The current (executing) user must already be a sudoer. If he is a sudoer with password required then * his password must be provided. */ -fun Prov.makeUserSudoerWithNoSudoPasswordRequired( +fun Prov.makeUserSudoerWithoutPasswordRequired( userName: String, password: Secret? = null, overwriteFile: Boolean = false -): ProvResult = task { +): ProvResult = taskWithResult { val userSudoFile = "/etc/sudoers.d/$userName" if (!checkFile(userSudoFile) || overwriteFile) { val sudoPrefix = if (password == null) "sudo" else "echo ${password.plain()} | sudo -S" @@ -107,11 +107,10 @@ fun Prov.makeUserSudoerWithNoSudoPasswordRequired( * Makes the current (executing) user be able to sudo without password. * IMPORTANT: Current user must already by sudoer when calling this function. */ -@Suppress("unused") // used externally -fun Prov.makeUserSudoerWithNoSudoPasswordRequired(password: Secret) = task { +fun Prov.makeCurrentUserSudoerWithoutPasswordRequired(password: Secret) = taskWithResult { val currentUser = whoami() if (currentUser != null) { - makeUserSudoerWithNoSudoPasswordRequired(currentUser, password, overwriteFile = true) + makeUserSudoerWithoutPasswordRequired(currentUser, password, overwriteFile = true) } else { ProvResult(false, "Current user could not be determined.") } diff --git a/src/test/kotlin/org/domaindrivenarchitecture/provs/configuration/domain/CliTargetCommandTest.kt b/src/test/kotlin/org/domaindrivenarchitecture/provs/configuration/domain/CliTargetCommandTest.kt index 0afe8b8..3b137c7 100644 --- a/src/test/kotlin/org/domaindrivenarchitecture/provs/configuration/domain/CliTargetCommandTest.kt +++ b/src/test/kotlin/org/domaindrivenarchitecture/provs/configuration/domain/CliTargetCommandTest.kt @@ -4,7 +4,7 @@ import io.mockk.* import org.domaindrivenarchitecture.provs.framework.core.Prov import org.domaindrivenarchitecture.provs.framework.core.Secret import org.domaindrivenarchitecture.provs.framework.core.cli.createProvInstance -import org.domaindrivenarchitecture.provs.framework.core.cli.retrievePassword +import org.domaindrivenarchitecture.provs.framework.core.cli.getPasswordToConfigureSudoWithoutPassword import org.domaindrivenarchitecture.provs.framework.core.local import org.domaindrivenarchitecture.provs.framework.core.processors.PrintOnlyProcessor import org.domaindrivenarchitecture.provs.framework.core.remote @@ -24,8 +24,8 @@ internal class CliTargetCommandKtTest { mockkStatic(::remote) every { remote(any(), any(), any(), any()) } returns Prov.newInstance(PrintOnlyProcessor()) - mockkStatic(::retrievePassword) - every { retrievePassword(any()) } returns Secret("sec") + mockkStatic(::getPasswordToConfigureSudoWithoutPassword) + every { getPasswordToConfigureSudoWithoutPassword() } returns Secret("sec") } @AfterAll @@ -34,7 +34,7 @@ internal class CliTargetCommandKtTest { unmockkObject(Prov) unmockkStatic(::local) unmockkStatic(::remote) - unmockkStatic(::retrievePassword) + unmockkStatic(::getPasswordToConfigureSudoWithoutPassword) } } diff --git a/src/test/kotlin/org/domaindrivenarchitecture/provs/desktop/application/ApplicationKtTest.kt b/src/test/kotlin/org/domaindrivenarchitecture/provs/desktop/application/ApplicationKtTest.kt index 25aadb7..0e260c8 100644 --- a/src/test/kotlin/org/domaindrivenarchitecture/provs/desktop/application/ApplicationKtTest.kt +++ b/src/test/kotlin/org/domaindrivenarchitecture/provs/desktop/application/ApplicationKtTest.kt @@ -7,7 +7,7 @@ import org.domaindrivenarchitecture.provs.configuration.domain.TargetCliCommand import org.domaindrivenarchitecture.provs.desktop.domain.* import org.domaindrivenarchitecture.provs.desktop.infrastructure.getConfig import org.domaindrivenarchitecture.provs.framework.core.* -import org.domaindrivenarchitecture.provs.framework.core.cli.retrievePassword +import org.domaindrivenarchitecture.provs.framework.core.cli.getPasswordToConfigureSudoWithoutPassword import org.domaindrivenarchitecture.provs.framework.core.processors.DummyProcessor import org.domaindrivenarchitecture.provs.test.setRootLoggingLevel import org.junit.jupiter.api.AfterAll @@ -52,8 +52,8 @@ internal class ApplicationKtTest { cmd = "mocked command" ) - mockkStatic(::retrievePassword) - every { retrievePassword(any()) } returns Secret("sec") + mockkStatic(::getPasswordToConfigureSudoWithoutPassword) + every { getPasswordToConfigureSudoWithoutPassword() } returns Secret("sec") } @Suppress("unused") // false positive @@ -65,7 +65,7 @@ internal class ApplicationKtTest { unmockkStatic(::remote) unmockkStatic(::getConfig) unmockkStatic(Prov::provisionDesktop) - unmockkStatic(::retrievePassword) + unmockkStatic(::getPasswordToConfigureSudoWithoutPassword) } }