-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
CORE-20012, CORE-20014: Create new plugin for profile commands (#6118)
Creates a new plugin for the CLI which allows management of a profile.yaml file. This is used to store various user profiles which consist of key-value pairs of configuration which will be used by the other CLI plugins.
- Loading branch information
1 parent
6471a0b
commit 5742828
Showing
9 changed files
with
626 additions
and
0 deletions.
There are no files selected for viewing
76 changes: 76 additions & 0 deletions
76
libs/corda-sdk/src/main/kotlin/net/corda/sdk/profile/ProfileUtils.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
package net.corda.sdk.profile | ||
|
||
import com.fasterxml.jackson.core.JsonProcessingException | ||
import com.fasterxml.jackson.databind.ObjectMapper | ||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory | ||
import com.fasterxml.jackson.module.kotlin.jacksonTypeRef | ||
import com.fasterxml.jackson.module.kotlin.registerKotlinModule | ||
import java.io.File | ||
import java.io.IOException | ||
|
||
data class CliProfile(val properties: Map<String, String>) | ||
|
||
enum class ProfileKey(val description: String) { | ||
REST_USERNAME("Username for REST API"), | ||
REST_PASSWORD("Password for REST API"), | ||
REST_ENDPOINT("Endpoint for the REST API"), | ||
JDBC_USERNAME("Username for JDBC connection"), | ||
JDBC_PASSWORD("Password for JDBC connection"), | ||
DATABASE_URL("URL for the database"); | ||
|
||
companion object { | ||
private val validKeys: List<String> by lazy { values().map { it.name.lowercase() } } | ||
private val cachedDescriptions: String by lazy { | ||
values().joinToString("\n") { key -> | ||
"${key.name.lowercase()}: ${key.description}," | ||
} | ||
} | ||
|
||
fun isValidKey(key: String): Boolean { | ||
return validKeys.contains(key.lowercase()) | ||
} | ||
|
||
fun getKeysWithDescriptions(): String { | ||
return cachedDescriptions | ||
} | ||
} | ||
} | ||
|
||
object ProfileUtils { | ||
private val objectMapper = ObjectMapper(YAMLFactory()).registerKotlinModule() | ||
private var profileFile: File | ||
|
||
init { | ||
profileFile = File(System.getProperty("user.home"), ".corda/cli/profile.yaml") | ||
} | ||
|
||
fun initialize(file: File) { | ||
profileFile = file | ||
} | ||
|
||
fun loadProfiles(): Map<String, CliProfile> { | ||
return try { | ||
if (profileFile.exists()) { | ||
val profilesMap = objectMapper.readValue(profileFile, jacksonTypeRef<Map<String, Map<String, String>>>()) | ||
profilesMap.mapValues { (_, properties) -> | ||
CliProfile(properties) | ||
} | ||
} else { | ||
emptyMap() | ||
} | ||
} catch (e: JsonProcessingException) { | ||
throw IllegalArgumentException("Invalid profile.yaml file format", e) | ||
} | ||
} | ||
|
||
fun saveProfiles(profiles: Map<String, CliProfile>) { | ||
try { | ||
val profilesMap = profiles.mapValues { (_, profile) -> | ||
profile.properties | ||
} | ||
objectMapper.writeValue(profileFile, profilesMap) | ||
} catch (e: IOException) { | ||
throw IOException("Failed to save profiles to file", e) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
plugins { | ||
id 'java' | ||
id 'distribution' | ||
id 'org.jetbrains.kotlin.jvm' | ||
id 'org.jetbrains.kotlin.kapt' | ||
id 'corda.cli-plugin-packager' | ||
id 'corda.common-publishing' | ||
} | ||
|
||
ext { | ||
releasable = false | ||
} | ||
|
||
ext.cordaEnableFormatting = true | ||
|
||
group 'net.corda.cli.deployment' | ||
|
||
dependencies { | ||
constraints { | ||
implementation(libs.slf4j.v2.api) | ||
} | ||
|
||
compileOnly "net.corda.cli.host:api:$pluginHostVersion" | ||
|
||
implementation libs.jackson.module.kotlin | ||
implementation libs.jackson.dataformat.yaml | ||
implementation libs.pf4j | ||
implementation project(":libs:configuration:configuration-core") | ||
implementation project(':libs:corda-sdk') | ||
kapt libs.pf4j | ||
|
||
testImplementation "net.corda.cli.host:api:$pluginHostVersion" | ||
testImplementation 'org.jetbrains.kotlin:kotlin-stdlib' | ||
testImplementation libs.pf4j | ||
testImplementation project(":testing:test-utilities") | ||
testImplementation project(":testing:packaging-test-utilities") | ||
|
||
testImplementation libs.bundles.test | ||
testImplementation 'org.jetbrains.kotlin:kotlin-stdlib' | ||
testRuntimeOnly libs.log4j.slf4j2 | ||
} | ||
|
||
test { | ||
useJUnitPlatform() | ||
} | ||
|
||
cliPlugin { | ||
cliPluginClass = 'net.corda.cli.plugins.profile.ProfilePluginWrapper' | ||
cliPluginDescription = 'Plugin for user-defined profile management.' | ||
} | ||
|
||
tasks.named("installDist") { | ||
dependsOn cliPlugin | ||
def homePath = System.properties['user.home'] | ||
from cliPlugin | ||
into "$homePath/.corda/cli/plugins" | ||
} | ||
|
||
publishing { | ||
publications { | ||
maven(MavenPublication) { | ||
artifactId "${cliPlugin.cliPluginId.get()}-cli-plugin" | ||
groupId project.group | ||
artifact cliPluginTask | ||
} | ||
} | ||
} |
46 changes: 46 additions & 0 deletions
46
tools/plugins/profile/src/main/kotlin/net/corda/cli/plugins/profile/ProfilePluginWrapper.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
package net.corda.cli.plugins.profile | ||
|
||
import net.corda.cli.api.AbstractCordaCliVersionProvider | ||
import net.corda.cli.api.CordaCliPlugin | ||
import net.corda.cli.plugins.profile.commands.CreateProfile | ||
import net.corda.cli.plugins.profile.commands.DeleteProfile | ||
import net.corda.cli.plugins.profile.commands.ListProfile | ||
import net.corda.cli.plugins.profile.commands.UpdateProfile | ||
import org.pf4j.Extension | ||
import org.pf4j.Plugin | ||
import org.slf4j.Logger | ||
import org.slf4j.LoggerFactory | ||
import picocli.CommandLine | ||
|
||
class VersionProvider : AbstractCordaCliVersionProvider() | ||
|
||
@Suppress("unused") | ||
class ProfilePluginWrapper : Plugin() { | ||
|
||
private companion object { | ||
val logger: Logger = LoggerFactory.getLogger(this::class.java.enclosingClass) | ||
} | ||
|
||
override fun start() { | ||
logger.debug("starting profile plugin") | ||
} | ||
|
||
override fun stop() { | ||
logger.debug("stopping profile plugin") | ||
} | ||
|
||
@Extension | ||
@CommandLine.Command( | ||
name = "profile", | ||
subcommands = [ | ||
CreateProfile::class, | ||
ListProfile::class, | ||
DeleteProfile::class, | ||
UpdateProfile::class, | ||
], | ||
mixinStandardHelpOptions = true, | ||
description = ["Plugin for profile operations."], | ||
versionProvider = VersionProvider::class | ||
) | ||
class PluginEntryPoint : CordaCliPlugin | ||
} |
70 changes: 70 additions & 0 deletions
70
...s/plugins/profile/src/main/kotlin/net/corda/cli/plugins/profile/commands/CreateProfile.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
package net.corda.cli.plugins.profile.commands | ||
|
||
import net.corda.libs.configuration.secret.SecretEncryptionUtil | ||
import net.corda.sdk.profile.CliProfile | ||
import net.corda.sdk.profile.ProfileKey | ||
import net.corda.sdk.profile.ProfileUtils | ||
import org.slf4j.Logger | ||
import org.slf4j.LoggerFactory | ||
import picocli.CommandLine | ||
import picocli.CommandLine.Option | ||
import java.util.UUID | ||
|
||
@CommandLine.Command( | ||
name = "create", | ||
description = ["Create a new profile."], | ||
mixinStandardHelpOptions = true | ||
) | ||
class CreateProfile : Runnable { | ||
|
||
private companion object { | ||
val logger: Logger = LoggerFactory.getLogger(this::class.java.enclosingClass) | ||
val sysOut: Logger = LoggerFactory.getLogger("SystemOut") | ||
val sysErr: Logger = LoggerFactory.getLogger("SystemErr") | ||
} | ||
|
||
@Option(names = ["-n", "--name"], description = ["Profile name"], required = true) | ||
lateinit var profileName: String | ||
|
||
@Option(names = ["-p", "--property"], description = ["Profile property (key=value)"], required = true) | ||
lateinit var properties: Array<String> | ||
|
||
private val secretEncryptionUtil = SecretEncryptionUtil() | ||
private val salt = UUID.randomUUID().toString() | ||
|
||
override fun run() { | ||
logger.debug("Creating profile: $profileName") | ||
val profiles = ProfileUtils.loadProfiles().toMutableMap() | ||
|
||
if (profiles.containsKey(profileName)) { | ||
sysOut.info("Profile '$profileName' already exists. Overwrite? (y/n)") | ||
val confirmation = readlnOrNull() | ||
if (confirmation?.lowercase() != "y") { | ||
sysOut.info("Profile creation aborted.") | ||
return | ||
} | ||
} | ||
|
||
val profileProperties = mutableMapOf<String, String>() | ||
properties.forEach { property -> | ||
val (key, value) = property.split("=") | ||
if (!ProfileKey.isValidKey(key)) { | ||
val error = "Invalid key '$key'. Allowed keys are:\n ${ProfileKey.getKeysWithDescriptions()}" | ||
sysErr.error(error) | ||
throw IllegalArgumentException(error) | ||
} | ||
if (key.lowercase().contains("password")) { | ||
val encryptedPassword = secretEncryptionUtil.encrypt(value, salt, salt) | ||
profileProperties[key] = encryptedPassword | ||
profileProperties["${key}_salt"] = salt | ||
} else { | ||
profileProperties[key] = value | ||
} | ||
} | ||
|
||
profiles[profileName] = CliProfile(profileProperties) | ||
|
||
ProfileUtils.saveProfiles(profiles) | ||
sysOut.info("Profile '$profileName' created successfully.") | ||
} | ||
} |
41 changes: 41 additions & 0 deletions
41
...s/plugins/profile/src/main/kotlin/net/corda/cli/plugins/profile/commands/DeleteProfile.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package net.corda.cli.plugins.profile.commands | ||
|
||
import net.corda.sdk.profile.ProfileUtils.loadProfiles | ||
import net.corda.sdk.profile.ProfileUtils.saveProfiles | ||
import org.slf4j.Logger | ||
import org.slf4j.LoggerFactory | ||
import picocli.CommandLine | ||
import picocli.CommandLine.Option | ||
|
||
@CommandLine.Command( | ||
name = "delete", | ||
description = ["Delete a profile."], | ||
mixinStandardHelpOptions = true | ||
) | ||
class DeleteProfile : Runnable { | ||
|
||
private companion object { | ||
val logger: Logger = LoggerFactory.getLogger(this::class.java.enclosingClass) | ||
val sysOut: Logger = LoggerFactory.getLogger("SystemOut") | ||
} | ||
|
||
@Option(names = ["-n", "--name"], description = ["Profile name"], required = true) | ||
lateinit var profileName: String | ||
|
||
override fun run() { | ||
logger.debug("Deleting profile: $profileName") | ||
val profiles = loadProfiles().toMutableMap() | ||
|
||
sysOut.info("Are you sure you want to delete profile '$profileName'? (y/n)") | ||
val confirmation = readlnOrNull() | ||
if (confirmation?.lowercase() != "y") { | ||
sysOut.info("Profile deletion aborted.") | ||
return | ||
} | ||
|
||
profiles.remove(profileName) | ||
|
||
saveProfiles(profiles) | ||
sysOut.info("Profile '$profileName' deleted.") | ||
} | ||
} |
66 changes: 66 additions & 0 deletions
66
tools/plugins/profile/src/main/kotlin/net/corda/cli/plugins/profile/commands/ListProfile.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
package net.corda.cli.plugins.profile.commands | ||
|
||
import net.corda.libs.configuration.secret.SecretEncryptionUtil | ||
import net.corda.sdk.profile.ProfileUtils.loadProfiles | ||
import org.slf4j.Logger | ||
import org.slf4j.LoggerFactory | ||
import picocli.CommandLine | ||
|
||
@CommandLine.Command( | ||
name = "list", | ||
description = ["List all profiles and profile properties."], | ||
mixinStandardHelpOptions = true | ||
) | ||
class ListProfile : Runnable { | ||
|
||
private companion object { | ||
val sysOut: Logger = LoggerFactory.getLogger("SystemOut") | ||
} | ||
|
||
@CommandLine.Option(names = ["-e", "--encrypted"], description = ["Show passwords in encrypted form"]) | ||
var showEncrypted: Boolean = false | ||
|
||
private val secretEncryptionUtil = SecretEncryptionUtil() | ||
|
||
@Suppress("NestedBlockDepth") | ||
private fun printProfile(profile: Map<String, String>) { | ||
profile.forEach { (key, value) -> | ||
if (key.lowercase().endsWith("_salt")) { | ||
// Skip printing the salt | ||
return@forEach | ||
} | ||
|
||
if (key.lowercase().contains("password")) { | ||
val salt = profile["${key}_salt"] | ||
if (salt != null) { | ||
if (showEncrypted) { | ||
sysOut.info(" $key: $value") | ||
} else { | ||
val decryptedPassword = secretEncryptionUtil.decrypt(value, salt, salt) | ||
sysOut.info(" $key: $decryptedPassword") | ||
} | ||
} else { | ||
sysOut.info(" $key: Unable to decrypt password") | ||
} | ||
} else { | ||
sysOut.info(" $key: $value") | ||
} | ||
} | ||
} | ||
|
||
override fun run() { | ||
val profiles = loadProfiles().toMutableMap() | ||
|
||
if (profiles.isEmpty()) { | ||
sysOut.info("No profiles found.") | ||
} else { | ||
sysOut.info("Available profiles:") | ||
profiles.keys.forEach { profileName -> | ||
sysOut.info("- $profileName") | ||
profiles[profileName]?.let { | ||
printProfile(it.properties) | ||
} | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.