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
Application -> DesktopCliCommand : isValid ?
Application -> CliUtils : createProvInstance
ProvInstance <- CliUtils : create
alt target.isValidLocal
CliUtils -> CliUtils : createLocalProv
else target.isValidRemote

View file

@ -17,7 +17,7 @@ fun main(args: Array<String>) {
exitProcess(1)
}
val prov = createProvInstance(cmd.target, remoteHostSetSudoWithoutPasswordRequired = true)
val prov = createProvInstance(cmd.target)
try {
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.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)
// a new session is required after making the user a sudoer without password
return remote(host, remoteUser, password)
return if (prov.currentUserCanSudoWithoutPassword()) {
prov
} 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()
}

View file

@ -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))
}

View file

@ -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)
}
}
}

View file

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

View file

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

View file

@ -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)
}
}

View file

@ -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)
}
}