@@ -2,20 +2,27 @@ package com.wafflestudio.spring.ocivault.config
22
33import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
44import com.fasterxml.jackson.module.kotlin.readValue
5- import com.oracle.bmc.Region
6- import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider
7- import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider
8- import com.oracle.bmc.secrets.SecretsClient
9- import com.oracle.bmc.secrets.model.Base64SecretBundleContentDetails
10- import com.oracle.bmc.secrets.requests.GetSecretBundleRequest
115import org.springframework.boot.SpringApplication
126import org.springframework.boot.env.EnvironmentPostProcessor
137import org.springframework.core.env.ConfigurableEnvironment
148import org.springframework.core.env.MapPropertySource
9+ import java.net.URI
10+ import java.net.http.HttpClient
11+ import java.net.http.HttpRequest
12+ import java.net.http.HttpResponse
13+ import java.nio.file.Files
14+ import java.nio.file.Path
15+ import java.security.KeyFactory
16+ import java.security.Signature
17+ import java.security.spec.PKCS8EncodedKeySpec
18+ import java.time.ZoneOffset
19+ import java.time.ZonedDateTime
20+ import java.time.format.DateTimeFormatter
1521import java.util.Base64
1622
1723class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor {
1824 private val objectMapper = jacksonObjectMapper()
25+ private val httpClient = HttpClient .newHttpClient()
1926
2027 override fun postProcessEnvironment (
2128 environment : ConfigurableEnvironment ,
@@ -27,27 +34,19 @@ class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor {
2734 }
2835 val secretIdsProperty = environment.getProperty(" oci-vault-secret-ids" ) ? : return
2936 val secretIds = secretIdsProperty.split(" ," ).map { it.trim() }
30- val region =
31- Region .fromRegionId(
32- environment.getProperty(" oci.vault.region" , " ap-chuncheon-1" ),
33- )
37+ val region = environment.getProperty(" oci.vault.region" , " ap-chuncheon-1" )
3438
35- val authProvider = createAuthProvider(environment)
36- val client = SecretsClient .builder().region(region).build(authProvider)
39+ val ociConfig = loadOciConfig()
3740 val secrets = mutableMapOf<String , Any >()
3841
39- try {
40- secretIds.forEach { secretId ->
41- val secretString = getSecretString(client, secretId)
42- val parsedSecrets = objectMapper.readValue<Map <String , Any >>(secretString)
43- secrets.putAll(
44- parsedSecrets.filterKeys {
45- environment.getProperty(it).isNullOrEmpty()
46- },
47- )
48- }
49- } finally {
50- client.close()
42+ secretIds.forEach { secretId ->
43+ val secretString = getSecretString(ociConfig, region, secretId)
44+ val parsedSecrets = objectMapper.readValue<Map <String , Any >>(secretString)
45+ secrets.putAll(
46+ parsedSecrets.filterKeys {
47+ environment.getProperty(it).isNullOrEmpty()
48+ },
49+ )
5150 }
5251
5352 if (secrets.isNotEmpty()) {
@@ -57,20 +56,100 @@ class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor {
5756 }
5857 }
5958
60- private fun createAuthProvider (environment : ConfigurableEnvironment ): BasicAuthenticationDetailsProvider {
61- return ConfigFileAuthenticationDetailsProvider (" DEFAULT" )
62- }
63-
6459 private fun getSecretString (
65- client : SecretsClient ,
60+ config : OciConfig ,
61+ region : String ,
6662 secretId : String ,
6763 ): String {
64+ val host = " secrets.vaults.$region .oci.oraclecloud.com"
65+ val path = " /20190301/secretbundles/$secretId "
66+ val date = DateTimeFormatter .RFC_1123_DATE_TIME .format(ZonedDateTime .now(ZoneOffset .UTC ))
67+
68+ val signingString = " (request-target): get $path \n date: $date \n host: $host "
69+ val signature = sign(config.privateKey, signingString)
70+ val keyId = " ${config.tenancy} /${config.user} /${config.fingerprint} "
71+ val authHeader =
72+ """ Signature version="1",keyId="$keyId ",algorithm="rsa-sha256",headers="(request-target) date host",signature="$signature """"
73+
6874 val request =
69- GetSecretBundleRequest .builder()
70- .secretId(secretId)
75+ HttpRequest .newBuilder()
76+ .uri(URI .create(" https://$host$path " ))
77+ .header(" date" , date)
78+ .header(" authorization" , authHeader)
79+ .GET ()
7180 .build()
72- val response = client.getSecretBundle(request)
73- val content = (response.secretBundle.secretBundleContent as Base64SecretBundleContentDetails ).content
81+
82+ val response = httpClient.send(request, HttpResponse .BodyHandlers .ofString())
83+ if (response.statusCode() != 200 ) {
84+ throw RuntimeException (" OCI Vault API error (${response.statusCode()} ): ${response.body()} " )
85+ }
86+
87+ val body = objectMapper.readValue<Map <String , Any >>(response.body())
88+
89+ @Suppress(" UNCHECKED_CAST" )
90+ val secretBundleContent =
91+ body[" secretBundleContent" ] as Map <String , Any >
92+ val content = secretBundleContent[" content" ] as String
7493 return String (Base64 .getDecoder().decode(content))
7594 }
95+
96+ private fun sign (
97+ privateKeyPem : String ,
98+ signingString : String ,
99+ ): String {
100+ val pemContent =
101+ privateKeyPem
102+ .substringAfter(" -----\n " )
103+ .substringBefore(" \n -----" )
104+ .replace(" \\ s" .toRegex(), " " )
105+ val keyBytes = Base64 .getDecoder().decode(pemContent)
106+ val keySpec = PKCS8EncodedKeySpec (keyBytes)
107+ val keyFactory = KeyFactory .getInstance(" RSA" )
108+ val key = keyFactory.generatePrivate(keySpec)
109+ val signature = Signature .getInstance(" SHA256withRSA" )
110+ signature.initSign(key)
111+ signature.update(signingString.toByteArray())
112+ return Base64 .getEncoder().encodeToString(signature.sign())
113+ }
114+
115+ private fun loadOciConfig (): OciConfig {
116+ val configPath = Path .of(System .getProperty(" user.home" ), " .oci" , " config" )
117+ val lines = Files .readAllLines(configPath)
118+ val props = mutableMapOf<String , String >()
119+ var inDefaultProfile = false
120+
121+ for (line in lines) {
122+ val trimmed = line.trim()
123+ if (trimmed == " [DEFAULT]" ) {
124+ inDefaultProfile = true
125+ continue
126+ }
127+ if (trimmed.startsWith(" [" )) {
128+ inDefaultProfile = false
129+ continue
130+ }
131+ if (inDefaultProfile && trimmed.contains(" =" )) {
132+ val (key, value) = trimmed.split(" =" , limit = 2 )
133+ props[key.trim()] = value.trim()
134+ }
135+ }
136+
137+ val keyFilePath =
138+ props[" key_file" ]?.replace(" ~" , System .getProperty(" user.home" ))
139+ ? : throw RuntimeException (" key_file not found in OCI config" )
140+
141+ return OciConfig (
142+ tenancy = props[" tenancy" ] ? : throw RuntimeException (" tenancy not found in OCI config" ),
143+ user = props[" user" ] ? : throw RuntimeException (" user not found in OCI config" ),
144+ fingerprint = props[" fingerprint" ] ? : throw RuntimeException (" fingerprint not found in OCI config" ),
145+ privateKey = Files .readString(Path .of(keyFilePath)),
146+ )
147+ }
148+
149+ private data class OciConfig (
150+ val tenancy : String ,
151+ val user : String ,
152+ val fingerprint : String ,
153+ val privateKey : String ,
154+ )
76155}
0 commit comments