2323import com .amazonaws .athena .connector .credentials .DefaultCredentials ;
2424import com .amazonaws .athena .connector .lambda .exceptions .AthenaConnectorException ;
2525import com .amazonaws .athena .connector .lambda .security .CachableSecretsManager ;
26+ import com .fasterxml .jackson .core .JsonProcessingException ;
2627import com .fasterxml .jackson .databind .JsonNode ;
2728import com .fasterxml .jackson .databind .ObjectMapper ;
2829import com .fasterxml .jackson .databind .node .ObjectNode ;
3233import software .amazon .awssdk .services .glue .model .ErrorDetails ;
3334import software .amazon .awssdk .services .glue .model .FederationSourceErrorCode ;
3435import software .amazon .awssdk .services .secretsmanager .SecretsManagerClient ;
36+ import software .amazon .awssdk .services .secretsmanager .model .EncryptionFailureException ;
37+ import software .amazon .awssdk .services .secretsmanager .model .InternalServiceErrorException ;
38+ import software .amazon .awssdk .services .secretsmanager .model .InvalidParameterException ;
39+ import software .amazon .awssdk .services .secretsmanager .model .InvalidRequestException ;
40+ import software .amazon .awssdk .services .secretsmanager .model .LimitExceededException ;
41+ import software .amazon .awssdk .services .secretsmanager .model .ResourceNotFoundException ;
42+ import software .amazon .awssdk .services .secretsmanager .model .SecretsManagerException ;
3543import software .amazon .awssdk .utils .Validate ;
3644
37- import java .io .BufferedReader ;
3845import java .io .IOException ;
39- import java .io .InputStream ;
40- import java .io .InputStreamReader ;
41- import java .io .OutputStream ;
42- import java .net .HttpURLConnection ;
43- import java .net .URL ;
46+ import java .net .URI ;
47+ import java .net .http .HttpClient ;
48+ import java .net .http .HttpRequest ;
49+ import java .net .http .HttpResponse ;
4450import java .nio .charset .StandardCharsets ;
4551import java .util .Base64 ;
4652import java .util .HashMap ;
4753import java .util .Map ;
48- import java .util .stream .Collectors ;
4954
5055/**
5156 * SAP HANA OAuth credentials provider that manages OAuth token lifecycle.
@@ -60,28 +65,38 @@ public class SaphanaCredentialsProvider implements CredentialsProvider
6065{
6166 private static final Logger LOGGER = LoggerFactory .getLogger (SaphanaCredentialsProvider .class );
6267
68+ // Constants for OAuth configuration fields
69+ private static final String CLIENT_ID = "client_id" ;
70+ private static final String CLIENT_SECRET = "client_secret" ;
71+ private static final String TOKEN_URL = "token_url" ;
72+
73+ // Constants for OAuth token response fields
6374 private static final String ACCESS_TOKEN = "access_token" ;
6475 private static final String FETCHED_AT = "fetched_at" ;
6576 private static final String EXPIRES_IN = "expires_in" ;
77+
78+ // Constants for basic authentication fields
6679 private static final String USERNAME = "username" ;
6780 private static final String PASSWORD = "password" ;
6881 private static final String USER = "user" ;
6982
7083 private final String oauthSecretName ;
7184 private final CachableSecretsManager secretsManager ;
7285 private final ObjectMapper objectMapper ;
86+ private final HttpClient httpClient ;
7387
7488 public SaphanaCredentialsProvider (String oauthSecretName )
7589 {
76- this (oauthSecretName , SecretsManagerClient .create ());
90+ this (oauthSecretName , SecretsManagerClient .create (), HttpClient . newHttpClient () );
7791 }
7892
7993 @ VisibleForTesting
80- public SaphanaCredentialsProvider (String oauthSecretName , SecretsManagerClient secretsClient )
94+ public SaphanaCredentialsProvider (String oauthSecretName , SecretsManagerClient secretsClient , HttpClient httpClient )
8195 {
8296 this .oauthSecretName = Validate .notNull (oauthSecretName , "oauthSecretName must not be null" );
8397 this .secretsManager = new CachableSecretsManager (secretsClient );
8498 this .objectMapper = new ObjectMapper ();
99+ this .httpClient = httpClient ;
85100 }
86101
87102 @ Override
@@ -132,12 +147,12 @@ public Map<String, String> getCredentialMap()
132147
133148 private boolean isOAuthConfigured (Map <String , String > oauthConfig )
134149 {
135- return oauthConfig .containsKey (SaphanaConstants . CLIENT_ID ) &&
136- !oauthConfig .get (SaphanaConstants . CLIENT_ID ).isEmpty () &&
137- oauthConfig .containsKey (SaphanaConstants . CLIENT_SECRET ) &&
138- !oauthConfig .get (SaphanaConstants . CLIENT_SECRET ).isEmpty () &&
139- oauthConfig .containsKey (SaphanaConstants . TOKEN_URL ) &&
140- !oauthConfig .get (SaphanaConstants . TOKEN_URL ).isEmpty ();
150+ return oauthConfig .containsKey (CLIENT_ID ) &&
151+ !oauthConfig .get (CLIENT_ID ).isEmpty () &&
152+ oauthConfig .containsKey (CLIENT_SECRET ) &&
153+ !oauthConfig .get (CLIENT_SECRET ).isEmpty () &&
154+ oauthConfig .containsKey (TOKEN_URL ) &&
155+ !oauthConfig .get (TOKEN_URL ).isEmpty ();
141156 }
142157
143158 private String loadTokenFromSecretsManager (Map <String , String > oauthConfig )
@@ -148,31 +163,32 @@ private String loadTokenFromSecretsManager(Map<String, String> oauthConfig)
148163 return null ;
149164 }
150165
151- private String fetchAndStoreNewToken (Map <String , String > oauthConfig ) throws Exception
166+ private String fetchAndStoreNewToken (Map <String , String > oauthConfig ) throws IOException , InterruptedException
152167 {
153- String clientId = oauthConfig .get (SaphanaConstants . CLIENT_ID );
154- String clientSecret = oauthConfig .get (SaphanaConstants . CLIENT_SECRET );
155- String tokenEndpoint = oauthConfig .get (SaphanaConstants . TOKEN_URL );
168+ String clientId = oauthConfig .get (CLIENT_ID );
169+ String clientSecret = oauthConfig .get (CLIENT_SECRET );
170+ String tokenEndpoint = oauthConfig .get (TOKEN_URL );
156171
157- HttpURLConnection conn = getHttpURLConnection (tokenEndpoint , clientId , clientSecret );
158- try (OutputStream os = conn .getOutputStream ()) {
159- byte [] input = "grant_type=client_credentials" .getBytes (StandardCharsets .UTF_8 );
160- os .write (input , 0 , input .length );
161- }
172+ String auth = clientId + ":" + clientSecret ;
173+ String encodedAuth = Base64 .getEncoder ().encodeToString (auth .getBytes (StandardCharsets .UTF_8 ));
162174
163- int responseCode = conn .getResponseCode ();
164- InputStream responseStream = (responseCode >= 200 && responseCode < 300 ) ?
165- conn .getInputStream () : conn .getErrorStream ();
175+ HttpRequest request = HttpRequest .newBuilder ()
176+ .uri (URI .create (tokenEndpoint ))
177+ .header ("Authorization" , "Basic " + encodedAuth )
178+ .header ("Content-Type" , "application/x-www-form-urlencoded" )
179+ .POST (HttpRequest .BodyPublishers .ofString ("grant_type=client_credentials" ))
180+ .build ();
166181
167- String responseBody = new BufferedReader ( new InputStreamReader ( responseStream ))
168- . lines ()
169- . collect ( Collectors . joining ( " \n " ) );
182+ HttpResponse < String > response = httpClient . send ( request , HttpResponse . BodyHandlers . ofString ());
183+ int responseCode = response . statusCode ();
184+ String responseBody = response . body ( );
170185
171186 if (responseCode != 200 ) {
172187 throw new AthenaConnectorException (
173- "Failed to obtain access token: " + responseCode + " - " + responseBody ,
188+ "Failed to obtain access token" ,
189+ "HTTP " + responseCode + " - " + responseBody ,
174190 ErrorDetails .builder ()
175- .errorCode (FederationSourceErrorCode .INTERNAL_SERVICE_EXCEPTION .toString ())
191+ .errorCode (FederationSourceErrorCode .INVALID_RESPONSE_EXCEPTION .toString ())
176192 .build ()
177193 );
178194 }
@@ -181,14 +197,15 @@ private String fetchAndStoreNewToken(Map<String, String> oauthConfig) throws Exc
181197 try {
182198 tokenResponse = objectMapper .readTree (responseBody );
183199 }
184- catch (Exception e ) {
200+ catch (JsonProcessingException e ) {
185201 throw new AthenaConnectorException (
186- "Failed to parse token response JSON " ,
202+ "Failed to parse OAuth token response" ,
187203 ErrorDetails .builder ()
188- .errorCode (FederationSourceErrorCode .INTERNAL_SERVICE_EXCEPTION .toString ())
204+ .errorCode (FederationSourceErrorCode .INVALID_RESPONSE_EXCEPTION .toString ())
189205 .build ()
190206 );
191207 }
208+
192209 String accessToken = tokenResponse .get (ACCESS_TOKEN ).asText ();
193210 long expiresIn = tokenResponse .get (EXPIRES_IN ).asLong ();
194211 long fetchedAt = System .currentTimeMillis () / 1000 ;
@@ -206,25 +223,70 @@ private String fetchAndStoreNewToken(Map<String, String> oauthConfig) throws Exc
206223 try {
207224 secretString = objectMapper .writeValueAsString (updatedSecretJson );
208225 }
209- catch (Exception e ) {
226+ catch (JsonProcessingException e ) {
210227 throw new AthenaConnectorException (
211- "Failed to serialize updated secret JSON " ,
228+ "Failed to parse OAuth token response " ,
212229 ErrorDetails .builder ()
213- .errorCode (FederationSourceErrorCode .INTERNAL_SERVICE_EXCEPTION .toString ())
230+ .errorCode (FederationSourceErrorCode .INVALID_RESPONSE_EXCEPTION .toString ())
214231 .build ()
215232 );
216233 }
217234
218- secretsManager .getSecretsManager ().putSecretValue (builder -> builder
219- .secretId (this .oauthSecretName )
220- .secretString (secretString )
221- .build ());
235+ try {
236+ secretsManager .getSecretsManager ().putSecretValue (builder -> builder
237+ .secretId (this .oauthSecretName )
238+ .secretString (secretString )
239+ .build ());
240+ }
241+ catch (ResourceNotFoundException e ) {
242+ throw new AthenaConnectorException (
243+ "Secret not found: " + oauthSecretName ,
244+ ErrorDetails .builder ()
245+ .errorCode (FederationSourceErrorCode .ENTITY_NOT_FOUND_EXCEPTION .toString ())
246+ .build ()
247+ );
248+ }
249+ catch (InvalidParameterException | InvalidRequestException e ) {
250+ throw new AthenaConnectorException (
251+ "Invalid request to Secrets Manager" ,
252+ e .getMessage (),
253+ ErrorDetails .builder ()
254+ .errorCode (FederationSourceErrorCode .INVALID_INPUT_EXCEPTION .toString ())
255+ .build ()
256+ );
257+ }
258+ catch (LimitExceededException e ) {
259+ throw new AthenaConnectorException (
260+ "Rate limit exceeded for Secrets Manager" ,
261+ e .getMessage (),
262+ ErrorDetails .builder ()
263+ .errorCode (FederationSourceErrorCode .THROTTLING_EXCEPTION .toString ())
264+ .build ()
265+ );
266+ }
267+ catch (EncryptionFailureException | InternalServiceErrorException e ) {
268+ throw new AthenaConnectorException (
269+ "AWS Secrets Manager internal error" ,
270+ e .getMessage (),
271+ ErrorDetails .builder ()
272+ .errorCode (FederationSourceErrorCode .INTERNAL_SERVICE_EXCEPTION .toString ())
273+ .build ()
274+ );
275+ }
276+ catch (SecretsManagerException e ) {
277+ throw new AthenaConnectorException (
278+ "Failed to update OAuth token in Secrets Manager" ,
279+ e .getMessage (),
280+ ErrorDetails .builder ()
281+ .errorCode (FederationSourceErrorCode .INTERNAL_SERVICE_EXCEPTION .toString ())
282+ .build ()
283+ );
284+ }
222285
223286 return accessToken ;
224287 }
225288
226- // Replace fetchAccessTokenFromSecret to use fetchAndStoreNewToken
227- private String fetchAccessTokenFromSecret (Map <String , String > oauthConfig ) throws Exception
289+ private String fetchAccessTokenFromSecret (Map <String , String > oauthConfig ) throws IOException , InterruptedException
228290 {
229291 String accessToken = loadTokenFromSecretsManager (oauthConfig );
230292 if (accessToken == null ) {
@@ -245,20 +307,4 @@ private String fetchAccessTokenFromSecret(Map<String, String> oauthConfig) throw
245307 }
246308 }
247309 }
248-
249- @ VisibleForTesting
250- static HttpURLConnection getHttpURLConnection (String tokenEndpoint , String clientId , String clientSecret ) throws IOException
251- {
252- URL url = new URL (tokenEndpoint );
253- HttpURLConnection conn = (HttpURLConnection ) url .openConnection ();
254-
255- String auth = clientId + ":" + clientSecret ;
256- String encodedAuth = Base64 .getEncoder ().encodeToString (auth .getBytes (StandardCharsets .UTF_8 ));
257-
258- conn .setRequestMethod ("POST" );
259- conn .setRequestProperty ("Authorization" , "Basic " + encodedAuth );
260- conn .setRequestProperty ("Content-Type" , "application/x-www-form-urlencoded" );
261- conn .setDoOutput (true );
262- return conn ;
263- }
264310}
0 commit comments