Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JPERF-729: Create a way to configure Jira admin user password during database setup #107

Merged
merged 26 commits into from
Jan 13, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a5f1e36
JPERF-729: Create a way to configure Jira admin user password during …
pczuj Jun 28, 2021
761f912
JPERF-729 Log dataset password update
mgrzaslewicz Nov 16, 2021
e71d277
JPERF-729 Fix unit test
mgrzaslewicz Nov 16, 2021
64b388b
JPERF-729 Handle both plain text and encrypted admin password dataset…
mgrzaslewicz Nov 19, 2021
8e2bd06
JPERF-729 Add changelog entry, deprecate Database.setup(ssh) instead …
mgrzaslewicz Nov 24, 2021
9e9fbc3
JPERF-729 Move changelog entry to unreleased, remove all API breaking…
mgrzaslewicz Nov 24, 2021
341818c
JPERF-729 Apply suggestions from PR comments
mgrzaslewicz Nov 24, 2021
9b8ae41
JPERF-729 Move username out of extension function to builder with def…
mgrzaslewicz Nov 25, 2021
10f9d35
JPERF-729 Move username out of extension function to builder with def…
mgrzaslewicz Nov 25, 2021
26aeb0e
JPERF-729 Make loggers non-static again to have the same approach eve…
mgrzaslewicz Nov 25, 2021
881f9b2
JPERF-729 Add link to issue in changelog
mgrzaslewicz Nov 25, 2021
fb2be73
JPERF-729 Unify logger creation
mgrzaslewicz Nov 25, 2021
56ad890
JPERF-729 Change log level to debug for password update
mgrzaslewicz Nov 25, 2021
0471a2a
JPERF-729 Extract password encryptor to follow single responsibility …
mgrzaslewicz Nov 25, 2021
13bcd56
JPERF-729 Fix typo in test method name
mgrzaslewicz Nov 25, 2021
4c49b61
JPERF-729 Refactor JiraUserPasswordOverridingDatabase
mgrzaslewicz Nov 29, 2021
bfc5e2e
JPERF-729 Change withAdminPassword extension function return type to …
mgrzaslewicz Dec 2, 2021
f5fb3b8
JPERF-729 Add .idea folder to gitignore
mgrzaslewicz Jan 5, 2022
64c957f
JPERF-729 Use java.util.Function instead of kotlin interface - for ja…
mgrzaslewicz Jan 11, 2022
614475f
JPERF-729 Simplify password encryptor API
mgrzaslewicz Jan 11, 2022
6009d11
JPERF-729 Simplify password encryptor API even more
mgrzaslewicz Jan 11, 2022
148c9ab
JPERF-729 Remove duplication in extension function creating builder
mgrzaslewicz Jan 11, 2022
9be53d5
JPERF-729 Use shorter names in builder
mgrzaslewicz Jan 11, 2022
16425ea
JPERF-729 Rename file containing JiraUserEncryptedPasswordProvider class
mgrzaslewicz Jan 11, 2022
bab7c59
JPERF-729 Move CrowdEncryptedPasswordProvider to a separate file
mgrzaslewicz Jan 12, 2022
9b2fbec
JPERF-729 Explicitly setup test context in JiraUserPasswordOverriding…
mgrzaslewicz Jan 12, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ Dropping a requirement of a major version of a dependency is a new contract.

## [Unreleased]
[Unreleased]: https://github.com/atlassian/infrastructure/compare/release-4.18.0...master
### Added
- `JiraUserPasswordOverridingDatabase` to support providing custom admin password during database setup [JPERF-729]

[JPERF-729]: https://ecosystem.atlassian.net/browse/JPERF-729

### Deprecated
- `Database.setup(ssh: SshConnection): String` in favor of `Database.performSetup(ssh: SshConnection): DatabaseSetup`

dagguh marked this conversation as resolved.
Show resolved Hide resolved
## [4.18.0] - 2021-04-14
[4.18.0]: https://github.com/atlassian/infrastructure/compare/release-4.17.5...release-4.18.0
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.atlassian.performance.tools.infrastructure.api.database

import com.atlassian.performance.tools.infrastructure.database.SshMysqlClient
import com.atlassian.performance.tools.infrastructure.database.SshSqlClient
import com.atlassian.performance.tools.ssh.api.SshConnection
import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger
import java.net.URI

class JiraUserPassword(
val plainText: String,
dagguh marked this conversation as resolved.
Show resolved Hide resolved
val encrypted: String
)

/**
* Based on https://confluence.atlassian.com/jira/retrieving-the-jira-administrator-192836.html
*
* To encode the password use [com.atlassian.crowd.password.encoder.AtlassianSecurityPasswordEncoder](https://docs.atlassian.com/atlassian-crowd/4.2.2/com/atlassian/crowd/password/encoder/AtlassianSecurityPasswordEncoder.html)
dagguh marked this conversation as resolved.
Show resolved Hide resolved
* from the [com.atlassian.crowd.crowd-password-encoders](https://mvnrepository.com/artifact/com.atlassian.crowd/crowd-password-encoders/4.2.2).
*/
class JiraUserPasswordOverridingDatabase internal constructor(
private val databaseDelegate: Database,
private val sqlClient: SshSqlClient,
private val username: String,
private val userPassword: JiraUserPassword,
private val jiraDatabaseSchemaName: String
) : Database {
dagguh marked this conversation as resolved.
Show resolved Hide resolved
private val logger: Logger = LogManager.getLogger(JiraUserPasswordOverridingDatabase::class.java)

override fun setup(ssh: SshConnection): String = databaseDelegate.setup(ssh)

override fun start(
jira: URI,
ssh: SshConnection
) {
databaseDelegate.start(jira, ssh)
if (shouldUseEncryption(ssh)) {
dagguh marked this conversation as resolved.
Show resolved Hide resolved
logger.info("Updating credential with encrypted password")
dagguh marked this conversation as resolved.
Show resolved Hide resolved
sqlClient.runSql(ssh, "UPDATE ${jiraDatabaseSchemaName}.cwd_user SET credential='${userPassword.encrypted}' WHERE user_name='$username';")
} else {
logger.info("Updating credential with plain text password")
sqlClient.runSql(ssh, "UPDATE ${jiraDatabaseSchemaName}.cwd_user SET credential='${userPassword.plainText}' WHERE user_name='$username';")
}
logger.info("Password for user '$username' updated to '${userPassword.plainText}'")
}

private fun shouldUseEncryption(ssh: SshConnection): Boolean {
val sqlResult =
sqlClient.runSql(ssh, "select attribute_value from ${jiraDatabaseSchemaName}.cwd_directory_attribute where attribute_name = 'user_encryption_method';").output
return when {
sqlResult.contains("plaintext") -> false
sqlResult.contains("atlassian-security") -> true
else -> {
logger.warn("Unknown user_encryption_method. Assuming encrypted password should be used")
true
}
}
}

class Builder(
private var databaseDelegate: Database,
private var userPassword: JiraUserPassword
) {
private var sqlClient: SshSqlClient = SshMysqlClient()
private var jiraDatabaseSchemaName: String = "jiradb"
private var username: String = "admin"

fun databaseDelegate(databaseDelegate: Database) = apply { this.databaseDelegate = databaseDelegate }
fun username(username: String) = apply { this.username = username }
fun userPassword(userPassword: JiraUserPassword) = apply { this.userPassword = userPassword }
fun sqlClient(sqlClient: SshSqlClient) = apply { this.sqlClient = sqlClient }
fun jiraDatabaseSchemaName(jiraDatabaseSchemaName: String) = apply { this.jiraDatabaseSchemaName = jiraDatabaseSchemaName }

fun build() = JiraUserPasswordOverridingDatabase(
databaseDelegate = databaseDelegate,
sqlClient = sqlClient,
username = username,
userPassword = userPassword,
jiraDatabaseSchemaName = jiraDatabaseSchemaName
)
}

}

fun Database.withAdminPassword(adminPassword: JiraUserPassword) = JiraUserPasswordOverridingDatabase.Builder(
databaseDelegate = this,
userPassword = adminPassword
).build()
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import org.apache.logging.log4j.Logger
import java.io.File
import java.net.URI
import java.nio.file.Files
import java.io.FileWriter
import java.io.BufferedWriter


/**
Expand All @@ -21,18 +19,17 @@ class LicenseOverridingMysql private constructor(
private val database: Database,
private val licenseCollection: LicenseCollection
) : Database {
private val logger: Logger = LogManager.getLogger(this::class.java)
private val logger: Logger = LogManager.getLogger(LicenseOverridingMysql::class.java)
dagguh marked this conversation as resolved.
Show resolved Hide resolved

@Deprecated(message = "Use the Builder and pass licenses as Files to reduce accidental leakage of the license")
constructor(
database: Database,
licenses: List<String>) : this(database, LicenseCollection(licenses.map<String, File> {
licenses: List<String>
) : this(database, LicenseCollection(licenses.map<String, File> {
createTempLicenseFile(it)
}))

override fun setup(
ssh: SshConnection
): String = database.setup(ssh)
override fun setup(ssh: SshConnection): String = database.setup(ssh)

override fun start(
jira: URI,
Expand Down Expand Up @@ -89,4 +86,4 @@ internal fun createTempLicenseFile(license: String): File {
.use { it.write(license) }
return licenseFile

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ class MySqlDatabase(
private val source: DatasetPackage,
private val maxConnections: Int
) : Database {
private val logger: Logger = LogManager.getLogger(this::class.java)
companion object {
private val logger: Logger = LogManager.getLogger(MySqlDatabase::class.java)
dagguh marked this conversation as resolved.
Show resolved Hide resolved
}

private val image: DockerImage = DockerImage(
name = "mysql:5.7.32",
Expand All @@ -36,13 +38,13 @@ class MySqlDatabase(
)

override fun setup(ssh: SshConnection): String {
val mysqlData = source.download(ssh)
val mysqlDataLocation = source.download(ssh)
image.run(
ssh = ssh,
parameters = "-p 3306:3306 -v `realpath $mysqlData`:/var/lib/mysql",
parameters = "-p 3306:3306 -v `realpath $mysqlDataLocation`:/var/lib/mysql",
arguments = "--skip-grant-tables --max_connections=$maxConnections"
)
return mysqlData
return mysqlDataLocation
}

override fun start(jira: URI, ssh: SshConnection) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package com.atlassian.performance.tools.infrastructure.api.database

import com.atlassian.performance.tools.infrastructure.mock.MockSshSqlClient
import com.atlassian.performance.tools.infrastructure.mock.RememberingDatabase
import com.atlassian.performance.tools.infrastructure.mock.RememberingSshConnection
import com.atlassian.performance.tools.ssh.api.SshConnection
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import java.net.URI

class JiraUserPasswordOverridingDatabaseTest {
dagguh marked this conversation as resolved.
Show resolved Hide resolved

private val jira = URI("http://localhost/")
private val samplePassword = JiraUserPassword(
plainText = "**plain text**",
encrypted = "**encrypted**"
)

@Test
fun shouldSetupUnderlyingDatabase() {
val underlyingDatabase = RememberingDatabase()
val database = JiraUserPasswordOverridingDatabase.Builder(
databaseDelegate = underlyingDatabase,
userPassword = samplePassword
).build()
val sshConnection = RememberingSshConnection()

database.setup(sshConnection)
database.start(jira, sshConnection)

assertThat(underlyingDatabase.isSetup)
.`as`("underlying database setup")
.isTrue()
}

@Test
fun shouldStartUnderlyingDatabase() {
val underlyingDatabase = RememberingDatabase()
val database = JiraUserPasswordOverridingDatabase.Builder(
databaseDelegate = underlyingDatabase,
userPassword = samplePassword
).build()
val sshConnection = RememberingSshConnection()

database.setup(sshConnection)
database.start(jira, sshConnection)

assertThat(underlyingDatabase.isStarted)
.`as`("underlying database started")
.isTrue()
}

@Test
fun shouldUpdateEncryptedPasswordByDefault() {
// given
val underlyingDatabase = RememberingDatabase()
val sqlClient = MockSshSqlClient()
val database = JiraUserPasswordOverridingDatabase(
databaseDelegate = underlyingDatabase,
sqlClient = sqlClient,
username = "admin",
userPassword = samplePassword,
jiraDatabaseSchemaName = "jira"
)
val sshConnection = RememberingSshConnection()

// when
database.setup(sshConnection)
database.start(jira, sshConnection)

// then
assertThat(sqlClient.getLog())
.`as`("sql queries executed")
.containsExactly(
"select attribute_value from jira.cwd_directory_attribute where attribute_name = 'user_encryption_method';",
"UPDATE jira.cwd_user SET credential='${samplePassword.encrypted}' WHERE user_name='admin';"
)
}

@Test
fun shouldUpdateEncryptedPassword() {
// given
val underlyingDatabase = RememberingDatabase()
val sqlClient = MockSshSqlClient()
val database = JiraUserPasswordOverridingDatabase(
databaseDelegate = underlyingDatabase,
sqlClient = sqlClient,
username = "admin",
userPassword = samplePassword,
jiraDatabaseSchemaName = "jiradb"
)
val sshConnection = RememberingSshConnection()
sqlClient.queueReturnedSqlCommandResult(
SshConnection.SshResult(
exitStatus = 0,
output = """attribute_value
atlassian-security
""".trimMargin(),
errorOutput = ""
)
)

// when
database.setup(sshConnection)
database.start(jira, sshConnection)

// then
assertThat(sqlClient.getLog())
.`as`("sql queries executed")
.containsExactly(
"select attribute_value from jiradb.cwd_directory_attribute where attribute_name = 'user_encryption_method';",
"UPDATE jiradb.cwd_user SET credential='${samplePassword.encrypted}' WHERE user_name='admin';"
)
}

@Test
fun shouldUpdatePlaintextPassword() {
// given
val underlyingDatabase = RememberingDatabase()
val sqlClient = MockSshSqlClient()
val database = JiraUserPasswordOverridingDatabase(
databaseDelegate = underlyingDatabase,
sqlClient = sqlClient,
username = "admin",
userPassword = samplePassword,
jiraDatabaseSchemaName = "jira"
)
val sshConnection = RememberingSshConnection()
sqlClient.queueReturnedSqlCommandResult(
SshConnection.SshResult(
exitStatus = 0,
output = """attribute_value
plaintext
""".trimMargin(),
errorOutput = ""
)
)

// when
database.setup(sshConnection)
database.start(jira, sshConnection)

// then
assertThat(sqlClient.getLog())
.`as`("sql queries executed")
.containsExactly(
"select attribute_value from jira.cwd_directory_attribute where attribute_name = 'user_encryption_method';",
"UPDATE jira.cwd_user SET credential='${samplePassword.plainText}' WHERE user_name='admin';"
)
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.atlassian.performance.tools.infrastructure.api.database

import com.atlassian.performance.tools.infrastructure.mock.RememberingDatabase
import com.atlassian.performance.tools.infrastructure.mock.RememberingSshConnection
import com.atlassian.performance.tools.ssh.api.SshConnection
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import java.io.File
Expand All @@ -11,14 +11,16 @@ class LicenseOverridingMysqlTest {

private val jira = URI("http://localhost/")

private fun Database.withLicenseString(licenses: List<String>) = LicenseOverridingMysql.Builder(this).licenseStrings(licenses).build()

@Test
fun shouldOverrideOneLicense() {
val licenseStrings = listOf("the only license")
val (testedDatabase, underlyingDatabase, ssh) = setUp(licenseStrings)

testedDatabase.start(jira, ssh)

assertThat(underlyingDatabase.started)
assertThat(underlyingDatabase.isStarted)
.`as`("underlying database started")
.isTrue()
assertSshCommands(ssh)
Expand Down Expand Up @@ -76,10 +78,7 @@ class LicenseOverridingMysqlTest {
val underlyingDatabase = RememberingDatabase()
@Suppress("DEPRECATION")
return DatabaseStartTest(
testedDatabase = LicenseOverridingMysql(
database = underlyingDatabase,
licenses = licenses
),
testedDatabase = underlyingDatabase.withLicenseString(licenses),
underlyingDatabase = underlyingDatabase,
ssh = RememberingSshConnection()
)
Expand All @@ -105,10 +104,7 @@ class LicenseOverridingMysqlTest {
val underlyingDatabase = RememberingDatabase()
@Suppress("DEPRECATION")
return DatabaseStartTest(
testedDatabase = LicenseOverridingMysql
.Builder(underlyingDatabase)
.licenseStrings(licenses)
.build(),
testedDatabase = underlyingDatabase.withLicenseString(licenses),
underlyingDatabase = underlyingDatabase,
ssh = RememberingSshConnection()
)
Expand Down Expand Up @@ -161,19 +157,4 @@ class LicenseOverridingMysqlTest {
val underlyingDatabase: RememberingDatabase,
val ssh: RememberingSshConnection
)

private class RememberingDatabase : Database {

var setup = false
var started = false

override fun setup(ssh: SshConnection): String {
setup = true
return "."
}

override fun start(jira: URI, ssh: SshConnection) {
started = true
}
}
}
Loading