001/* 002 * Copyright 2025 devteam@scivics-lab.com 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, 011 * software distributed under the License is distributed on an 012 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 013 * either express or implied. See the License for the 014 * specific language governing permissions and limitations 015 * under the License. 016 */ 017 018package com.scivicslab.actoriac; 019 020import org.yaml.snakeyaml.Yaml; 021 022import java.io.IOException; 023import java.net.URI; 024import java.net.http.HttpClient; 025import java.net.http.HttpRequest; 026import java.net.http.HttpResponse; 027import java.time.Duration; 028import java.util.Map; 029 030/** 031 * Client for HashiCorp Vault API communication. 032 * Supports reading secrets from Vault KV v2 engine. 033 * 034 * @author devteam@scivics-lab.com 035 */ 036public class VaultClient { 037 private final VaultConfig config; 038 private final HttpClient httpClient; 039 040 /** 041 * Creates a new VaultClient with the given configuration. 042 * 043 * @param config Vault configuration 044 */ 045 public VaultClient(VaultConfig config) { 046 this.config = config; 047 this.httpClient = HttpClient.newBuilder() 048 .connectTimeout(Duration.ofSeconds(10)) 049 .build(); 050 } 051 052 /** 053 * Reads a secret from Vault. 054 * 055 * @param path Secret path (e.g., "secret/data/ssh/iacuser/private_key") 056 * @return Secret value as String 057 * @throws VaultException if Vault communication fails or secret not found 058 */ 059 public String readSecret(String path) throws VaultException { 060 try { 061 String url = config.getAddress() + "/v1/" + path; 062 063 HttpRequest request = HttpRequest.newBuilder() 064 .uri(URI.create(url)) 065 .header("X-Vault-Token", config.getToken()) 066 .GET() 067 .build(); 068 069 HttpResponse<String> response = httpClient.send(request, 070 HttpResponse.BodyHandlers.ofString()); 071 072 if (response.statusCode() == 404) { 073 throw new VaultException("Secret not found at path: " + path); 074 } 075 076 if (response.statusCode() != 200) { 077 throw new VaultException("Vault returned status " + response.statusCode() + 078 ": " + response.body()); 079 } 080 081 return extractSecretValue(response.body()); 082 083 } catch (IOException | InterruptedException e) { 084 throw new VaultException("Failed to read secret from Vault: " + e.getMessage(), e); 085 } 086 } 087 088 /** 089 * Extracts the secret value from Vault API response. 090 * 091 * For KV v2 engine, the response structure is: 092 * { 093 * "data": { 094 * "data": { 095 * "value": "actual-secret-value" 096 * } 097 * } 098 * } 099 * 100 * @param jsonResponse JSON response from Vault 101 * @return Secret value 102 * @throws VaultException if response format is invalid 103 */ 104 @SuppressWarnings("unchecked") 105 private String extractSecretValue(String jsonResponse) throws VaultException { 106 try { 107 Yaml yaml = new Yaml(); 108 Map<String, Object> response = yaml.load(jsonResponse); 109 110 Object dataObj = response.get("data"); 111 if (!(dataObj instanceof Map)) { 112 throw new VaultException("Invalid Vault response: 'data' field missing or not a map"); 113 } 114 115 Map<String, Object> data = (Map<String, Object>) dataObj; 116 Object innerDataObj = data.get("data"); 117 if (!(innerDataObj instanceof Map)) { 118 throw new VaultException("Invalid Vault response: nested 'data' field missing or not a map"); 119 } 120 121 Map<String, Object> innerData = (Map<String, Object>) innerDataObj; 122 Object value = innerData.get("value"); 123 if (value == null) { 124 throw new VaultException("Invalid Vault response: 'value' field missing"); 125 } 126 127 return value.toString(); 128 129 } catch (Exception e) { 130 throw new VaultException("Failed to parse Vault response: " + e.getMessage(), e); 131 } 132 } 133 134 /** 135 * Exception thrown when Vault operations fail. 136 */ 137 public static class VaultException extends Exception { 138 public VaultException(String message) { 139 super(message); 140 } 141 142 public VaultException(String message, Throwable cause) { 143 super(message, cause); 144 } 145 } 146}