Skip to content

Commit

Permalink
♻ : better use of dependency injection
Browse files Browse the repository at this point in the history
* extract an interface for CredentialsService
* move VaultCredentialsService to its own class
* remove NoopEncryptionService
  • Loading branch information
juwit committed Aug 24, 2020
1 parent 00397ef commit 3bfcfd4
Show file tree
Hide file tree
Showing 9 changed files with 333 additions and 265 deletions.
108 changes: 28 additions & 80 deletions src/main/java/io/gaia_app/credentials/CredentialsService.kt
Original file line number Diff line number Diff line change
@@ -1,96 +1,44 @@
package io.gaia_app.credentials

import com.fasterxml.jackson.annotation.JsonAlias
import io.gaia_app.encryption.EncryptionService
import io.gaia_app.teams.User
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.stereotype.Service
import org.springframework.vault.core.VaultTemplate
import java.util.*

@Service
class CredentialsService(val credentialsRepository: CredentialsRepository, val encryptionService: EncryptionService) {

@Autowired(required = false)
var vaultTemplate: VaultTemplate? = null

fun findById(id: String): Optional<Credentials> = this.credentialsRepository.findById(id).map { decrypt(it) }

fun findByIdAndCreatedBy(id: String, createdBy: User): Optional<Credentials> = credentialsRepository.findByIdAndCreatedBy(id, createdBy).map { decrypt(it) }

fun findAllByCreatedBy(createdBy: User) = credentialsRepository.findAllByCreatedBy(createdBy)

fun save(credentials: Credentials): Credentials {
return encrypt(credentials).apply { credentialsRepository.save(credentials) }
}

fun deleteById(id: String) = credentialsRepository.deleteById(id)

fun encrypt(it: Credentials): Credentials = when (it) {
is AWSCredentials -> encryptionService.encryptAwsCredentials(it)
is GoogleCredentials -> encryptionService.encryptGoogleCredentials(it)
is AzureRMCredentials -> encryptionService.encryptAzurermCredentials(it)
// vault credentials does no need encryption
is VaultAWSCredentials -> it
}

fun decrypt(it: Credentials): Credentials = when (it) {
is AWSCredentials -> encryptionService.decryptAwsCredentials(it)
is GoogleCredentials -> encryptionService.decryptGoogleCredentials(it)
is AzureRMCredentials -> encryptionService.decryptAzurermCredentials(it)
is VaultAWSCredentials -> loadAWSCredentialsFromVault(it)
}

fun loadAWSCredentialsFromVault(vaultAWSCredentials: VaultAWSCredentials): AWSCredentials {
val path = "${vaultAWSCredentials.vaultAwsSecretEnginePath.trimEnd('/')}/creds/${vaultAWSCredentials.vaultAwsRole}"
val vaultResponse = vaultTemplate!!.read(path, VaultAWSResponse::class.java)

// IAM credentials are eventually consistent with respect to other Amazon services.
// adding a delay of 5 seconds before returning them
runBlocking {
delay(5_000)
}

return vaultResponse?.data?.toAWSCredentials() ?: throw RuntimeException("Unable to get AWS credentials from Vault")
}
interface CredentialsService {
/**
* finds the given credentials from the database.
* depending on implementation, this method could decrypt credentials.
*/
fun findById(id: String): Optional<Credentials>
fun findByIdAndCreatedBy(id: String, createdBy: User): Optional<Credentials>
fun findAllByCreatedBy(createdBy: User): List<EmptyCredentials>
fun save(credentials: Credentials): Credentials
fun deleteById(id: String)

/**
* loads the given credentials.
* depending on implementation, this method could fetch or decrypt the credentials from somewhere.
* this method should be called by the runner when credentials are needed to run jobs
*/
fun load(id: String): Optional<Credentials>
}

data class VaultAWSResponse(@JsonAlias("access_key") val accessKey: String, @JsonAlias("secret_key") val secretKey: String) {
fun toAWSCredentials() = AWSCredentials(accessKey, secretKey)
}
@Service
@ConditionalOnMissingBean(EncryptionService::class)
class PlainCredentialsService(val credentialsRepository: CredentialsRepository): CredentialsService {

fun EncryptionService.encryptAwsCredentials(awsCredentials: AWSCredentials): Credentials {
awsCredentials.accessKey = this.encrypt(awsCredentials.accessKey)
awsCredentials.secretKey = this.encrypt(awsCredentials.secretKey)
return awsCredentials
}
override fun findById(id: String): Optional<Credentials> = credentialsRepository.findById(id)

fun EncryptionService.decryptAwsCredentials(awsCredentials: AWSCredentials): Credentials {
awsCredentials.accessKey = this.decrypt(awsCredentials.accessKey)
awsCredentials.secretKey = this.decrypt(awsCredentials.secretKey)
return awsCredentials
}
override fun findByIdAndCreatedBy(id: String, createdBy: User): Optional<Credentials> = credentialsRepository.findByIdAndCreatedBy(id, createdBy)

fun EncryptionService.encryptGoogleCredentials(googleCredentials: GoogleCredentials): Credentials {
googleCredentials.serviceAccountJSONContents = this.encrypt(googleCredentials.serviceAccountJSONContents)
return googleCredentials
}
override fun findAllByCreatedBy(createdBy: User): List<EmptyCredentials> = credentialsRepository.findAllByCreatedBy(createdBy)

fun EncryptionService.decryptGoogleCredentials(googleCredentials: GoogleCredentials): Credentials {
googleCredentials.serviceAccountJSONContents = this.decrypt(googleCredentials.serviceAccountJSONContents)
return googleCredentials
}
override fun save(credentials: Credentials): Credentials = credentialsRepository.save(credentials)

fun EncryptionService.encryptAzurermCredentials(azureRMCredentials: AzureRMCredentials): Credentials {
azureRMCredentials.clientId = this.encrypt(azureRMCredentials.clientId)
azureRMCredentials.clientSecret = this.encrypt(azureRMCredentials.clientSecret)
return azureRMCredentials
}
override fun deleteById(id: String) = credentialsRepository.deleteById(id)

override fun load(id: String): Optional<Credentials> = this.findById(id)

fun EncryptionService.decryptAzurermCredentials(azureRMCredentials: AzureRMCredentials): Credentials {
azureRMCredentials.clientId = this.decrypt(azureRMCredentials.clientId)
azureRMCredentials.clientSecret = this.decrypt(azureRMCredentials.clientSecret)
return azureRMCredentials
}
21 changes: 0 additions & 21 deletions src/main/java/io/gaia_app/encryption/EncryptionService.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
package io.gaia_app.encryption

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

/**
* Service for encryption/decryption of data
*/
Expand All @@ -14,20 +10,3 @@ interface EncryptionService {
fun decrypt(cipherText: String): String

}

@Configuration
class NoOpEncryptionServiceConfig {

@Bean
@ConditionalOnMissingBean(EncryptionService::class)
fun noopEncryptionService() = NoOpEncryptionService()

}

class NoOpEncryptionService:EncryptionService{

override fun encrypt(plaintext: String) = plaintext

override fun decrypt(cipherText: String) = cipherText

}
6 changes: 3 additions & 3 deletions src/main/java/io/gaia_app/runner/StackRunner.java
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ private void treatJob(JobWorkflow jobWorkflow, Consumer<JobWorkflow> jobActionFn
@Async
public void plan(JobWorkflow jobWorkflow, TerraformModule module, Stack stack) {
if(stack.getCredentialsId() != null){
jobWorkflow.getJob().setCredentials(this.credentialsService.findById(stack.getCredentialsId()).orElseThrow());
jobWorkflow.getJob().setCredentials(this.credentialsService.load(stack.getCredentialsId()).orElseThrow());
}
treatJob(
jobWorkflow,
Expand All @@ -136,7 +136,7 @@ public void plan(JobWorkflow jobWorkflow, TerraformModule module, Stack stack) {
@Async
public void apply(JobWorkflow jobWorkflow, TerraformModule module, Stack stack) {
if(stack.getCredentialsId() != null){
jobWorkflow.getJob().setCredentials(this.credentialsService.findById(stack.getCredentialsId()).orElseThrow());
jobWorkflow.getJob().setCredentials(this.credentialsService.load(stack.getCredentialsId()).orElseThrow());
}
treatJob(
jobWorkflow,
Expand All @@ -149,7 +149,7 @@ public void apply(JobWorkflow jobWorkflow, TerraformModule module, Stack stack)
@Async
public void retry(JobWorkflow jobWorkflow, TerraformModule module, Stack stack) {
if(stack.getCredentialsId() != null){
jobWorkflow.getJob().setCredentials(this.credentialsService.findById(stack.getCredentialsId()).orElseThrow());
jobWorkflow.getJob().setCredentials(this.credentialsService.load(stack.getCredentialsId()).orElseThrow());
}
stepRepository.deleteByJobId(jobWorkflow.getJob().getId());
treatJob(
Expand Down
107 changes: 107 additions & 0 deletions src/main/java/io/gaia_app/vault/VaultCredentialsService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package io.gaia_app.vault

import com.fasterxml.jackson.annotation.JsonAlias
import io.gaia_app.credentials.*
import io.gaia_app.encryption.EncryptionService
import io.gaia_app.teams.User
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean
import org.springframework.stereotype.Service
import org.springframework.vault.core.VaultTemplate
import java.util.*

@Service
@ConditionalOnBean(VaultEncryptionService::class)
class VaultCredentialsService(val credentialsRepository: CredentialsRepository,
val encryptionService: EncryptionService,
val vaultTemplate: VaultTemplate) : CredentialsService {

override fun findById(id: String): Optional<Credentials> = this.credentialsRepository.findById(id).map { decrypt(it) }

override fun findByIdAndCreatedBy(id: String, createdBy: User): Optional<Credentials> = credentialsRepository.findByIdAndCreatedBy(id, createdBy).map { decrypt(it) }

override fun findAllByCreatedBy(createdBy: User) = credentialsRepository.findAllByCreatedBy(createdBy)

override fun save(credentials: Credentials): Credentials {
return encrypt(credentials).apply { credentialsRepository.save(credentials) }
}

override fun deleteById(id: String) = credentialsRepository.deleteById(id)

override fun load(id: String): Optional<Credentials> = this.credentialsRepository.findById(id).map { load(it) }

fun encrypt(it: Credentials): Credentials = when (it) {
// we encrypt only credentials that contains sensitive information
is AWSCredentials -> encryptionService.encryptAwsCredentials(it)
is GoogleCredentials -> encryptionService.encryptGoogleCredentials(it)
is AzureRMCredentials -> encryptionService.encryptAzurermCredentials(it)
// vault credentials does no need encryption as we only store ids
is VaultAWSCredentials -> it
}

fun decrypt(it: Credentials): Credentials = when (it) {
// we decrypt only credentials that contains sensitive information
is AWSCredentials -> encryptionService.decryptAwsCredentials(it)
is GoogleCredentials -> encryptionService.decryptGoogleCredentials(it)
is AzureRMCredentials -> encryptionService.decryptAzurermCredentials(it)
else -> it
}

fun load(it: Credentials): Credentials = when (it) {
// vault credentials need to be fetched from vault, other type simply need decryption
is VaultAWSCredentials -> loadAWSCredentialsFromVault(it)
else -> decrypt(it)
}

fun loadAWSCredentialsFromVault(vaultAWSCredentials: VaultAWSCredentials): AWSCredentials {
val path = "${vaultAWSCredentials.vaultAwsSecretEnginePath.trimEnd('/')}/creds/${vaultAWSCredentials.vaultAwsRole}"
val vaultResponse = vaultTemplate!!.read(path, VaultAWSResponse::class.java)

// IAM credentials are eventually consistent with respect to other Amazon services.
// adding a delay of 5 seconds before returning them
runBlocking {
delay(5_000)
}

return vaultResponse?.data?.toAWSCredentials() ?: throw RuntimeException("Unable to get AWS credentials from Vault")
}
}

data class VaultAWSResponse(@JsonAlias("access_key") val accessKey: String, @JsonAlias("secret_key") val secretKey: String) {
fun toAWSCredentials() = AWSCredentials(accessKey, secretKey)
}

fun EncryptionService.encryptAwsCredentials(awsCredentials: AWSCredentials): Credentials {
awsCredentials.accessKey = this.encrypt(awsCredentials.accessKey)
awsCredentials.secretKey = this.encrypt(awsCredentials.secretKey)
return awsCredentials
}

fun EncryptionService.decryptAwsCredentials(awsCredentials: AWSCredentials): Credentials {
awsCredentials.accessKey = this.decrypt(awsCredentials.accessKey)
awsCredentials.secretKey = this.decrypt(awsCredentials.secretKey)
return awsCredentials
}

fun EncryptionService.encryptGoogleCredentials(googleCredentials: GoogleCredentials): Credentials {
googleCredentials.serviceAccountJSONContents = this.encrypt(googleCredentials.serviceAccountJSONContents)
return googleCredentials
}

fun EncryptionService.decryptGoogleCredentials(googleCredentials: GoogleCredentials): Credentials {
googleCredentials.serviceAccountJSONContents = this.decrypt(googleCredentials.serviceAccountJSONContents)
return googleCredentials
}

fun EncryptionService.encryptAzurermCredentials(azureRMCredentials: AzureRMCredentials): Credentials {
azureRMCredentials.clientId = this.encrypt(azureRMCredentials.clientId)
azureRMCredentials.clientSecret = this.encrypt(azureRMCredentials.clientSecret)
return azureRMCredentials
}

fun EncryptionService.decryptAzurermCredentials(azureRMCredentials: AzureRMCredentials): Credentials {
azureRMCredentials.clientId = this.decrypt(azureRMCredentials.clientId)
azureRMCredentials.clientSecret = this.decrypt(azureRMCredentials.clientSecret)
return azureRMCredentials
}

0 comments on commit 3bfcfd4

Please sign in to comment.