Merge branch 'local-sudoer-without-pw' into 'master'

Local sudoer without pw

See merge request domaindrivenarchitecture/provs!6
This commit is contained in:
ansgarz 2023-02-26 19:57:21 +00:00
commit c82abbb3db
10 changed files with 122 additions and 71 deletions

View file

@ -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
```

View file

@ -24,7 +24,6 @@ CliArgumentsParser -> DesktopCliCommand : create(desktopType, cliTargetCmd, ...)
CliArgumentsParser --> Application: desktopCliCommand CliArgumentsParser --> Application: desktopCliCommand
Application -> DesktopCliCommand : isValid ? Application -> DesktopCliCommand : isValid ?
Application -> CliUtils : createProvInstance Application -> CliUtils : createProvInstance
ProvInstance <- CliUtils : create
alt target.isValidLocal alt target.isValidLocal
CliUtils -> CliUtils : createLocalProv CliUtils -> CliUtils : createLocalProv
else target.isValidRemote else target.isValidRemote

View file

@ -17,7 +17,7 @@ fun main(args: Array<String>) {
exitProcess(1) exitProcess(1)
} }
val prov = createProvInstance(cmd.target, remoteHostSetSudoWithoutPasswordRequired = true) val prov = createProvInstance(cmd.target)
try { try {
provisionDesktopCommand(prov, cmd) provisionDesktopCommand(prov, cmd)

View file

@ -7,42 +7,39 @@ 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.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 org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.whoami
import kotlin.system.exitProcess import kotlin.system.exitProcess
/** /**
* Returns a Prov instance according to the targetCommand. * 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. * 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( fun createProvInstance(targetCommand: TargetCliCommand): Prov {
targetCommand: TargetCliCommand,
remoteHostSetSudoWithoutPasswordRequired: Boolean = false
): Prov {
if (targetCommand.isValid()) { if (targetCommand.isValid()) {
val password: Secret? = targetCommand.remoteTarget()?.password val password: Secret? = targetCommand.remoteTarget()?.password
val remoteTarget = targetCommand.remoteTarget() val remoteTarget = targetCommand.remoteTarget()
if (targetCommand.isValidLocalhost()) {
return createLocalProvInstance() return if (targetCommand.isValidLocalhost()) {
createLocalProvInstance()
} else if (targetCommand.isValidRemote() && remoteTarget != null) { } else if (targetCommand.isValidRemote() && remoteTarget != null) {
return createRemoteProvInstance( createRemoteProvInstance(
remoteTarget.host, remoteTarget.host,
remoteTarget.user, remoteTarget.user,
remoteTarget.password == null, remoteTarget.password == null,
password, password
remoteHostSetSudoWithoutPasswordRequired
) )
} else { } 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 { } 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) exitProcess(1)
} }
} }
@ -50,9 +47,13 @@ fun createProvInstance(
private fun createLocalProvInstance(): Prov { private fun createLocalProvInstance(): Prov {
val prov = local() val prov = local()
if (!prov.currentUserCanSudoWithoutPassword()) { if (!prov.currentUserCanSudoWithoutPassword()) {
val password = PromptSecretSource("Please enter password to configure sudo without password in the future." + val passwordNonNull = getPasswordToConfigureSudoWithoutPassword()
"\nWarning: This will permanently allow your user to use sudo privileges without a password.").secret()
prov.makeUserSudoerWithNoSudoPasswordRequired(password) prov.makeCurrentUserSudoerWithoutPasswordRequired(passwordNonNull)
check(prov.currentUserCanSudoWithoutPassword()) {
"ERROR: User ${prov.whoami()} cannot sudo without enteringa password."
}
} }
return prov return prov
} }
@ -62,42 +63,43 @@ private fun createRemoteProvInstance(
host: String, host: String,
remoteUser: String, remoteUser: String,
sshWithKey: Boolean, sshWithKey: Boolean,
password: Secret?, password: Secret?
remoteHostSetSudoWithoutPasswordRequired: Boolean
): Prov { ): Prov {
val prov = val prov =
if (sshWithKey) { if (sshWithKey) {
remote(host, remoteUser) remote(host, remoteUser)
} else { } else {
require( require(password != null) {
password != null, "No password available for provisioning without ssh keys. " +
{ "No password available for provisioning without ssh keys. Either specify provisioning by ssh-keys or provide password." }) "Either specify provisioning by ssh-keys or provide password."
}
remote(host, remoteUser, password) remote(host, remoteUser, password)
} }
if (!prov.currentUserCanSudoWithoutPassword()) { return if (prov.currentUserCanSudoWithoutPassword()) {
if (remoteHostSetSudoWithoutPasswordRequired) { prov
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)
// a new session is required after making the user a sudoer without password
return remote(host, remoteUser, password)
} else { } 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 getPasswordToConfigureSudoWithoutPassword(): Secret {
internal fun retrievePassword(cliCommand: TargetCliCommand): Secret? { return PromptSecretSource("password to configure sudo without password.").secret()
var password: Secret? = null
if (cliCommand.isValidRemote() && cliCommand.passwordInteractive) {
password =
PromptSecretSource("Password for user $cliCommand.userName!! on $cliCommand.remoteHost!!").secret()
}
return password
} }

View file

@ -13,13 +13,6 @@ class UbuntuProv internal constructor(
progressType: ProgressType progressType: ProgressType
) : Prov(processor, name, 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 { override fun cmd(cmd: String, dir: String?, sudo: Boolean): ProvResult = taskWithResult {
exec(SHELL, "-c", commandWithDirAndSudo(cmd, dir, sudo)) exec(SHELL, "-c", commandWithDirAndSudo(cmd, dir, sudo))
} }

View file

@ -38,6 +38,8 @@ class RemoteProcessor(host: InetAddress, user: String, password: Secret? = null)
// Attention: host key is not verified // Attention: host key is not verified
ssh.addHostKeyVerifier(PromiscuousVerifier()) ssh.addHostKeyVerifier(PromiscuousVerifier())
ssh.connectTimeout = 30000 // ms
ssh.connect(host) ssh.connect(host)
if (password != null) { if (password != null) {
@ -50,8 +52,9 @@ class RemoteProcessor(host: InetAddress, user: String, password: Secret? = null)
try { try {
ssh.disconnect() ssh.disconnect()
} finally { } finally {
log.error("Got exception when initializing ssh (Username, password or ssh-key might be wrong): " + e.message) val errorMag = "Error when initializing ssh (Host, username, password or ssh-key might be wrong) "
throw RuntimeException("Error when initializing ssh (Username, password or ssh-key might be wrong) ", e) log.error(errorMag + e.message)
throw RuntimeException(errorMag, e)
} }
} }
} }

View file

@ -8,7 +8,7 @@ import javax.swing.*
class PasswordPanel : JPanel(FlowLayout()) { class PasswordPanel : JPanel(FlowLayout()) {
private val passwordField = JPasswordField(20) private val passwordField = JPasswordField(30)
private var entered = false private var entered = false
val enteredPassword val enteredPassword

View file

@ -33,7 +33,7 @@ fun Prov.createUser(
} }
password?.let { cmdNoLog("sudo echo \"$userName:${password.plain()}\" | sudo chpasswd") } ?: ProvResult(true) password?.let { cmdNoLog("sudo echo \"$userName:${password.plain()}\" | sudo chpasswd") } ?: ProvResult(true)
if (userCanSudoWithoutPassword) { if (userCanSudoWithoutPassword) {
makeUserSudoerWithNoSudoPasswordRequired(userName) makeUserSudoerWithoutPasswordRequired(userName)
} }
val authorizedKeysFile = userHome() + ".ssh/authorized_keys" val authorizedKeysFile = userHome() + ".ssh/authorized_keys"
if (copyAuthorizedSshKeysFromCurrentUser && checkFile(authorizedKeysFile)) { 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 * The current (executing) user must already be a sudoer. If he is a sudoer with password required then
* his password must be provided. * his password must be provided.
*/ */
fun Prov.makeUserSudoerWithNoSudoPasswordRequired( fun Prov.makeUserSudoerWithoutPasswordRequired(
userName: String, userName: String,
password: Secret? = null, password: Secret? = null,
overwriteFile: Boolean = false overwriteFile: Boolean = false
): ProvResult = task { ): ProvResult = taskWithResult {
val userSudoFile = "/etc/sudoers.d/$userName" val userSudoFile = "/etc/sudoers.d/$userName"
if (!checkFile(userSudoFile) || overwriteFile) { if (!checkFile(userSudoFile) || overwriteFile) {
val sudoPrefix = if (password == null) "sudo" else "echo ${password.plain()} | sudo -S" 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. * Makes the current (executing) user be able to sudo without password.
* IMPORTANT: Current user must already by sudoer when calling this function. * IMPORTANT: Current user must already by sudoer when calling this function.
*/ */
@Suppress("unused") // used externally fun Prov.makeCurrentUserSudoerWithoutPasswordRequired(password: Secret) = taskWithResult {
fun Prov.makeUserSudoerWithNoSudoPasswordRequired(password: Secret) = task {
val currentUser = whoami() val currentUser = whoami()
if (currentUser != null) { if (currentUser != null) {
makeUserSudoerWithNoSudoPasswordRequired(currentUser, password, overwriteFile = true) makeUserSudoerWithoutPasswordRequired(currentUser, password, overwriteFile = true)
} else { } else {
ProvResult(false, "Current user could not be determined.") ProvResult(false, "Current user could not be determined.")
} }

View file

@ -4,7 +4,7 @@ import io.mockk.*
import org.domaindrivenarchitecture.provs.framework.core.Prov import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.core.Secret import org.domaindrivenarchitecture.provs.framework.core.Secret
import org.domaindrivenarchitecture.provs.framework.core.cli.createProvInstance 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.local
import org.domaindrivenarchitecture.provs.framework.core.processors.PrintOnlyProcessor import org.domaindrivenarchitecture.provs.framework.core.processors.PrintOnlyProcessor
import org.domaindrivenarchitecture.provs.framework.core.remote import org.domaindrivenarchitecture.provs.framework.core.remote
@ -24,8 +24,8 @@ internal class CliTargetCommandKtTest {
mockkStatic(::remote) mockkStatic(::remote)
every { remote(any(), any(), any(), any()) } returns Prov.newInstance(PrintOnlyProcessor()) every { remote(any(), any(), any(), any()) } returns Prov.newInstance(PrintOnlyProcessor())
mockkStatic(::retrievePassword) mockkStatic(::getPasswordToConfigureSudoWithoutPassword)
every { retrievePassword(any()) } returns Secret("sec") every { getPasswordToConfigureSudoWithoutPassword() } returns Secret("sec")
} }
@AfterAll @AfterAll
@ -34,7 +34,7 @@ internal class CliTargetCommandKtTest {
unmockkObject(Prov) unmockkObject(Prov)
unmockkStatic(::local) unmockkStatic(::local)
unmockkStatic(::remote) unmockkStatic(::remote)
unmockkStatic(::retrievePassword) unmockkStatic(::getPasswordToConfigureSudoWithoutPassword)
} }
} }

View file

@ -7,7 +7,7 @@ import org.domaindrivenarchitecture.provs.configuration.domain.TargetCliCommand
import org.domaindrivenarchitecture.provs.desktop.domain.* import org.domaindrivenarchitecture.provs.desktop.domain.*
import org.domaindrivenarchitecture.provs.desktop.infrastructure.getConfig import org.domaindrivenarchitecture.provs.desktop.infrastructure.getConfig
import org.domaindrivenarchitecture.provs.framework.core.* 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.framework.core.processors.DummyProcessor
import org.domaindrivenarchitecture.provs.test.setRootLoggingLevel import org.domaindrivenarchitecture.provs.test.setRootLoggingLevel
import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.AfterAll
@ -52,8 +52,8 @@ internal class ApplicationKtTest {
cmd = "mocked command" cmd = "mocked command"
) )
mockkStatic(::retrievePassword) mockkStatic(::getPasswordToConfigureSudoWithoutPassword)
every { retrievePassword(any()) } returns Secret("sec") every { getPasswordToConfigureSudoWithoutPassword() } returns Secret("sec")
} }
@Suppress("unused") // false positive @Suppress("unused") // false positive
@ -65,7 +65,7 @@ internal class ApplicationKtTest {
unmockkStatic(::remote) unmockkStatic(::remote)
unmockkStatic(::getConfig) unmockkStatic(::getConfig)
unmockkStatic(Prov::provisionDesktop) unmockkStatic(Prov::provisionDesktop)
unmockkStatic(::retrievePassword) unmockkStatic(::getPasswordToConfigureSudoWithoutPassword)
} }
} }