From 43247d88f5b7503463af3e78bc02762764db4140 Mon Sep 17 00:00:00 2001 From: Marcel Jacek Date: Tue, 29 Jul 2025 18:05:36 +0200 Subject: [PATCH 01/12] add prototype keyflow implementation --- .../cloud/stackit/sdk/core/ApiException.java | 152 ++++++++++++++++++ .../sdk/core/KeyFlowAuthenticator.java | 152 ++++++++++++++++++ .../sdk/core/keyflow/KeyFlowInterceptor.java | 27 ++++ .../core/model/ServiceAccountCredentials.java | 65 ++++++++ .../sdk/core/model/ServiceAccountKey.java | 102 ++++++++++++ 5 files changed, 498 insertions(+) create mode 100644 core/src/main/java/cloud/stackit/sdk/core/ApiException.java create mode 100644 core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java create mode 100644 core/src/main/java/cloud/stackit/sdk/core/keyflow/KeyFlowInterceptor.java create mode 100644 core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountCredentials.java create mode 100644 core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java diff --git a/core/src/main/java/cloud/stackit/sdk/core/ApiException.java b/core/src/main/java/cloud/stackit/sdk/core/ApiException.java new file mode 100644 index 0000000..d3cb409 --- /dev/null +++ b/core/src/main/java/cloud/stackit/sdk/core/ApiException.java @@ -0,0 +1,152 @@ +package cloud.stackit.sdk.core; + + +import java.util.List; +import java.util.Map; + +/** + *

ApiException class.

+ */ +public class ApiException extends Exception { + private static final long serialVersionUID = 1L; + + private int code = 0; + private Map> responseHeaders = null; + private String responseBody = null; + + /** + *

Constructor for ApiException.

+ */ + public ApiException() {} + + /** + *

Constructor for ApiException.

+ * + * @param throwable a {@link java.lang.Throwable} object + */ + public ApiException(Throwable throwable) { + super(throwable); + } + + /** + *

Constructor for ApiException.

+ * + * @param message the error message + */ + public ApiException(String message) { + super(message); + } + + /** + *

Constructor for ApiException.

+ * + * @param message the error message + * @param throwable a {@link java.lang.Throwable} object + * @param code HTTP status code + * @param responseHeaders a {@link java.util.Map} of HTTP response headers + * @param responseBody the response body + */ + public ApiException(String message, Throwable throwable, int code, Map> responseHeaders, String responseBody) { + super(message, throwable); + this.code = code; + this.responseHeaders = responseHeaders; + this.responseBody = responseBody; + } + + /** + *

Constructor for ApiException.

+ * + * @param message the error message + * @param code HTTP status code + * @param responseHeaders a {@link java.util.Map} of HTTP response headers + * @param responseBody the response body + */ + public ApiException(String message, int code, Map> responseHeaders, String responseBody) { + this(message, null, code, responseHeaders, responseBody); + } + + /** + *

Constructor for ApiException.

+ * + * @param message the error message + * @param throwable a {@link java.lang.Throwable} object + * @param code HTTP status code + * @param responseHeaders a {@link java.util.Map} of HTTP response headers + */ + public ApiException(String message, Throwable throwable, int code, Map> responseHeaders) { + this(message, throwable, code, responseHeaders, null); + } + + /** + *

Constructor for ApiException.

+ * + * @param code HTTP status code + * @param responseHeaders a {@link java.util.Map} of HTTP response headers + * @param responseBody the response body + */ + public ApiException(int code, Map> responseHeaders, String responseBody) { + this("Response Code: " + code + " Response Body: " + responseBody, null, code, responseHeaders, responseBody); + } + + /** + *

Constructor for ApiException.

+ * + * @param code HTTP status code + * @param message a {@link java.lang.String} object + */ + public ApiException(int code, String message) { + super(message); + this.code = code; + } + + /** + *

Constructor for ApiException.

+ * + * @param code HTTP status code + * @param message the error message + * @param responseHeaders a {@link java.util.Map} of HTTP response headers + * @param responseBody the response body + */ + public ApiException(int code, String message, Map> responseHeaders, String responseBody) { + this(code, message); + this.responseHeaders = responseHeaders; + this.responseBody = responseBody; + } + + /** + * Get the HTTP status code. + * + * @return HTTP status code + */ + public int getCode() { + return code; + } + + /** + * Get the HTTP response headers. + * + * @return A map of list of string + */ + public Map> getResponseHeaders() { + return responseHeaders; + } + + /** + * Get the HTTP response body. + * + * @return Response body in the form of string + */ + public String getResponseBody() { + return responseBody; + } + + /** + * Get the exception message including HTTP response data. + * + * @return The exception message + */ + public String getMessage() { + return String.format("Message: %s%nHTTP response code: %s%nHTTP response body: %s%nHTTP response headers: %s", + super.getMessage(), this.getCode(), this.getResponseBody(), this.getResponseHeaders()); + } +} diff --git a/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java new file mode 100644 index 0000000..f73a472 --- /dev/null +++ b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java @@ -0,0 +1,152 @@ +package cloud.stackit.sdk.core; + +import cloud.stackit.sdk.core.model.ServiceAccountCredentials; +import cloud.stackit.sdk.core.model.ServiceAccountKey; +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; +import okhttp3.*; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.nio.charset.StandardCharsets; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +public class KeyFlowAuthenticator { + private final String REFRESH_TOKEN = "refresh_token"; + private final String ASSERTION = "assertion"; + + private final OkHttpClient httpClient; + private final ServiceAccountKey saKey; + private KeyFlowTokenResponse token; + private final Gson gson; + private final String tokenUrl; + + private static class KeyFlowTokenResponse { + @SerializedName("access_token") + private String accessToken; + @SerializedName("refresh_token") + private String refreshToken; + @SerializedName("expires_in") + private long expiresIn; + @SerializedName("scope") + private String scope; + @SerializedName("token_type") + private String tokenType; + + public boolean isExpired() { + return expiresIn < new Date().toInstant().minusSeconds(60).getEpochSecond(); + } + + public String getAccessToken() { + return accessToken; + } + } + + public KeyFlowAuthenticator(ServiceAccountKey saKey) { + this.saKey = saKey; + this.gson = new Gson(); + this.httpClient = new OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build(); + this.tokenUrl = "https://service-account.api.stackit.cloud/token"; + createAccessToken(); + } + + + public synchronized String getAccessToken() throws IOException { + if (token == null || token.isExpired()) { + createAccessTokenWithRefreshToken(); + } + return token.getAccessToken(); + } + + private void createAccessToken() { + String grant = "urn:ietf:params:oauth:grant-type:jwt-bearer"; + String assertion = generateSelfSignedJWT(); + try(Response response = requestToken(grant, assertion).execute()) { + parseTokenResponse(response); + } catch (IOException | ApiException e) { + e.printStackTrace(); + } + } + + private synchronized void createAccessTokenWithRefreshToken() throws IOException { + String refreshToken = token.refreshToken; + try (Response response = requestToken(REFRESH_TOKEN, refreshToken).execute()) { + parseTokenResponse(response); + } catch (ApiException e) { + e.printStackTrace(); + } + } + + private synchronized void parseTokenResponse(Response response) throws ApiException { + if (response.code() != HttpURLConnection.HTTP_OK) { + String body = null; + if (response.body() != null) { + body = response.body().toString(); + response.body().close(); + } + throw new ApiException(response.message(), response.code(), response.headers().toMultimap(), body); + } + if (response.body() == null) { + throw new ApiException("body from token creation is null"); + } + + token = gson.fromJson(new InputStreamReader(response.body().byteStream(), StandardCharsets.UTF_8), KeyFlowTokenResponse.class); + token.expiresIn = JWT.decode(token.accessToken).getExpiresAt().toInstant().getEpochSecond(); + response.body().close(); + } + + private Call requestToken(String grant, String assertion) throws IOException { + FormBody.Builder bodyBuilder = new FormBody.Builder(); + bodyBuilder.addEncoded("grant_type", grant); + if (grant.equals(REFRESH_TOKEN)) { + bodyBuilder.addEncoded(REFRESH_TOKEN, assertion); + } else { + bodyBuilder.addEncoded(ASSERTION, assertion); + } + FormBody body = bodyBuilder.build(); + + Request request = new Request.Builder() + .url(tokenUrl) + .post(body) + .addHeader("Content-Type", "application/x-www-form-urlencoded") + .build(); + return httpClient.newCall(request); + } + + private String generateSelfSignedJWT() { + RSAPrivateKey prvKey = saKey.getCredentials().getPrivateKeyParsed(); + Algorithm algorithm = null; + try { + algorithm = Algorithm.RSA512(prvKey); + } catch (Exception e) { + e.printStackTrace(); + } + + Map jwtHeader = new HashMap<>(); + jwtHeader.put("kid", saKey.getCredentials().getKid()); + + return JWT.create() + .withIssuer(saKey.getCredentials().getIss()) + .withSubject(saKey.getCredentials().getSub().toString()) + .withJWTId(UUID.randomUUID().toString()) + .withAudience(saKey.getCredentials().getAud()) + .withIssuedAt(new Date()) + .withExpiresAt(new Date().toInstant().plusSeconds(10 * 60)) + .withHeader(jwtHeader) + .sign(algorithm); + } +} diff --git a/core/src/main/java/cloud/stackit/sdk/core/keyflow/KeyFlowInterceptor.java b/core/src/main/java/cloud/stackit/sdk/core/keyflow/KeyFlowInterceptor.java new file mode 100644 index 0000000..d253936 --- /dev/null +++ b/core/src/main/java/cloud/stackit/sdk/core/keyflow/KeyFlowInterceptor.java @@ -0,0 +1,27 @@ +package cloud.stackit.sdk.core.keyflow; + +import cloud.stackit.sdk.core.KeyFlowAuthenticator; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.IOException; + +public class KeyFlowInterceptor implements Interceptor { + private final KeyFlowAuthenticator authenticator; + + public KeyFlowInterceptor(KeyFlowAuthenticator authenticator) { + this.authenticator = authenticator; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request originalRequest = chain.request(); + String accessToken = authenticator.getAccessToken(); + + Request authenticatedRequest = originalRequest.newBuilder() + .header("Authorization", "Bearer " + accessToken) + .build(); + return chain.proceed(authenticatedRequest); + } +} diff --git a/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountCredentials.java b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountCredentials.java new file mode 100644 index 0000000..bdf42cc --- /dev/null +++ b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountCredentials.java @@ -0,0 +1,65 @@ +package cloud.stackit.sdk.core.model; + +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; +import java.util.UUID; + +public class ServiceAccountCredentials { + private final String aud; + private final String iss; + private final String kid; + private final String privateKey; + private final UUID sub; + + public ServiceAccountCredentials(String aud, String iss, String kid, String privateKey, UUID sub) { + this.aud = aud; + this.iss = iss; + this.kid = kid; + this.privateKey = privateKey; + this.sub = sub; + } + + public String getAud() { + return aud; + } + + public String getIss() { + return iss; + } + + public String getKid() { + return kid; + } + + public String getPrivateKey() { + return privateKey; + } + + public UUID getSub() { + return sub; + } + + public RSAPrivateKey getPrivateKeyParsed() { + RSAPrivateKey prvKey = null; + try { + String trimmedKey = privateKey.replaceFirst("-----BEGIN PRIVATE KEY-----", ""); + trimmedKey = trimmedKey.replaceFirst("-----END PRIVATE KEY-----", ""); + trimmedKey = trimmedKey.replaceAll("\n",""); + + byte[] privateBytes = Base64.getDecoder().decode(trimmedKey); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + prvKey = (RSAPrivateKey) keyFactory.generatePrivate(keySpec); + } catch (InvalidKeySpecException e) { + e.printStackTrace(); + } catch (NoSuchAlgorithmException e) { + System.out.println(e); + } + return prvKey; + } +} diff --git a/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java new file mode 100644 index 0000000..99d695d --- /dev/null +++ b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java @@ -0,0 +1,102 @@ +package cloud.stackit.sdk.core.model; + +import com.google.gson.Gson; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; +import java.util.Date; + +public class ServiceAccountKey { + private final String id; + private final String publicKey; + private final Date created; + private final String keyType; + private final String keyOrigin; + private final String keyAlgorithm; + private final boolean active; + private final Date validUntil; + private final ServiceAccountCredentials credentials; + + + public ServiceAccountKey(String id, String publicKey, Date created, String keyType, String keyOrigin, String keyAlgorithm, boolean active, Date validUntil, ServiceAccountCredentials credentials) { + this.id = id; + this.publicKey = publicKey; + this.created = created; + this.keyType = keyType; + this.keyOrigin = keyOrigin; + this.keyAlgorithm = keyAlgorithm; + this.active = active; + this.validUntil = validUntil; + this.credentials = credentials; + } + + public String getId() { + return id; + } + + public String getPublicKey() { + return publicKey; + } + + public Date getCreated() { + return created; + } + + public String getKeyType() { + return keyType; + } + + public String getKeyOrigin() { + return keyOrigin; + } + + public String getKeyAlgorithm() { + return keyAlgorithm; + } + + public boolean isActive() { + return active; + } + + public Date getValidUntil() { + return validUntil; + } + + public ServiceAccountCredentials getCredentials() { + return credentials; + } + + public RSAPublicKey getPublicKeyParsed() { + RSAPublicKey pubKey = null; + try { + String trimmedKey = publicKey.replaceFirst("-----BEGIN PUBLIC KEY-----", ""); + trimmedKey = trimmedKey.replaceFirst("-----END PUBLIC KEY-----", ""); + trimmedKey = trimmedKey.replaceAll("\n",""); + + byte[] publicBytes = Base64.getDecoder().decode(trimmedKey); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + pubKey = (RSAPublicKey) keyFactory.generatePublic(keySpec); + } catch (InvalidKeySpecException e) { + e.printStackTrace(); + } catch (NoSuchAlgorithmException e) { + System.out.println(e); + } + return pubKey; + } + +// public static ServiceAccountKey loadCredentials(InputStream jsonStream) { +// return new Gson().fromJson(new InputStreamReader(jsonStream, StandardCharsets.UTF_8), ServiceAccountKey.class); +// } + public static ServiceAccountKey loadCredentials(String json) { + return new Gson().fromJson(json, ServiceAccountKey.class); + } +} From 3b2eb24bdc564e2d0f810951403c53ce2aa8f1ef Mon Sep 17 00:00:00 2001 From: Marcel Jacek Date: Wed, 30 Jul 2025 18:32:31 +0200 Subject: [PATCH 02/12] add configuration for ApiClient --- .../sdk/core/KeyFlowAuthenticator.java | 4 - .../{keyflow => }/KeyFlowInterceptor.java | 5 +- .../stackit/sdk/core/auth/SetupAuth.java | 151 ++++++++++++++++++ .../sdk/core/config/Configuration.java | 124 ++++++++++++++ .../sdk/core/config/EnvironmentVariables.java | 30 ++++ .../core/model/ServiceAccountCredentials.java | 11 +- .../sdk/core/model/ServiceAccountKey.java | 10 +- 7 files changed, 318 insertions(+), 17 deletions(-) rename core/src/main/java/cloud/stackit/sdk/core/{keyflow => }/KeyFlowInterceptor.java (89%) create mode 100644 core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java create mode 100644 core/src/main/java/cloud/stackit/sdk/core/config/Configuration.java create mode 100644 core/src/main/java/cloud/stackit/sdk/core/config/EnvironmentVariables.java diff --git a/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java index f73a472..ecd4c1f 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java +++ b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java @@ -1,6 +1,5 @@ package cloud.stackit.sdk.core; -import cloud.stackit.sdk.core.model.ServiceAccountCredentials; import cloud.stackit.sdk.core.model.ServiceAccountKey; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; @@ -9,12 +8,10 @@ import okhttp3.*; import java.io.IOException; -import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.nio.charset.StandardCharsets; import java.security.interfaces.RSAPrivateKey; -import java.security.interfaces.RSAPublicKey; import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -64,7 +61,6 @@ public KeyFlowAuthenticator(ServiceAccountKey saKey) { createAccessToken(); } - public synchronized String getAccessToken() throws IOException { if (token == null || token.isExpired()) { createAccessTokenWithRefreshToken(); diff --git a/core/src/main/java/cloud/stackit/sdk/core/keyflow/KeyFlowInterceptor.java b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowInterceptor.java similarity index 89% rename from core/src/main/java/cloud/stackit/sdk/core/keyflow/KeyFlowInterceptor.java rename to core/src/main/java/cloud/stackit/sdk/core/KeyFlowInterceptor.java index d253936..77305b1 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/keyflow/KeyFlowInterceptor.java +++ b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowInterceptor.java @@ -1,9 +1,9 @@ -package cloud.stackit.sdk.core.keyflow; +package cloud.stackit.sdk.core; -import cloud.stackit.sdk.core.KeyFlowAuthenticator; import okhttp3.Interceptor; import okhttp3.Request; import okhttp3.Response; +import org.jetbrains.annotations.NotNull; import java.io.IOException; @@ -14,6 +14,7 @@ public KeyFlowInterceptor(KeyFlowAuthenticator authenticator) { this.authenticator = authenticator; } + @NotNull @Override public Response intercept(Chain chain) throws IOException { Request originalRequest = chain.request(); diff --git a/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java b/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java new file mode 100644 index 0000000..d1b1d08 --- /dev/null +++ b/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java @@ -0,0 +1,151 @@ +package cloud.stackit.sdk.core.auth; + +import cloud.stackit.sdk.core.KeyFlowAuthenticator; +import cloud.stackit.sdk.core.config.Configuration; +import cloud.stackit.sdk.core.config.EnvironmentVariables; +import cloud.stackit.sdk.core.KeyFlowInterceptor; +import cloud.stackit.sdk.core.model.ServiceAccountKey; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import okhttp3.Interceptor; + +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Map; + +public class SetupAuth { + private Interceptor authHandler; + private final String defaultCredentialsFilePath = "~/.stackit/credentials.json"; + + public SetupAuth() { + this(new Configuration.Builder().build()); + } + + public SetupAuth(Configuration cfg) { + if (cfg == null) { + cfg = new Configuration.Builder().build(); + } + + try { + ServiceAccountKey saKey = setupKeyFlow(cfg); + authHandler = new KeyFlowInterceptor(new KeyFlowAuthenticator(saKey)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public Interceptor getAuthHandler() { + return authHandler; + } + + private ServiceAccountKey setupKeyFlow(Configuration cfg) throws Exception { + ServiceAccountKey saKey = null; + // Explicit config in code + if (cfg.getServiceAccountKey() != null && !cfg.getServiceAccountKey().trim().isEmpty()) { + saKey = ServiceAccountKey.loadCredentials(cfg.getServiceAccountKey()); + loadPrivateKey(cfg, saKey); + return saKey; + } + + if (cfg.getServiceAccountKeyPath() != null && !cfg.getServiceAccountKeyPath().trim().isEmpty()) { + String fileContent = new String(Files.readAllBytes(Paths.get(cfg.getServiceAccountKeyPath())), StandardCharsets.UTF_8); + saKey = new Gson().fromJson(fileContent, ServiceAccountKey.class); + loadPrivateKey(cfg, saKey); + return saKey; + } + + // Env config + if (!EnvironmentVariables.STACKIT_SERVICE_ACCOUNT_KEY.trim().isEmpty()) { + saKey = ServiceAccountKey.loadCredentials(EnvironmentVariables.STACKIT_SERVICE_ACCOUNT_KEY.trim()); + loadPrivateKey(cfg, saKey); + return saKey; + } + + if (!EnvironmentVariables.STACKIT_SERVICE_ACCOUNT_KEY_PATH.trim().isEmpty()) { + String fileContent = new String(Files.readAllBytes(Paths.get(cfg.getServiceAccountKeyPath())), StandardCharsets.UTF_8); + saKey = new Gson().fromJson(fileContent, ServiceAccountKey.class); + loadPrivateKey(cfg, saKey); + return saKey; + } + + if (!EnvironmentVariables.STACKIT_CREDENTIALS_PATH.trim().isEmpty()) { + String saKeyJson = readValueFromCredentialsFile(EnvironmentVariables.STACKIT_CREDENTIALS_PATH, EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY, EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY_PATH); + saKey = new Gson().fromJson(saKeyJson, ServiceAccountKey.class); + loadPrivateKey(cfg, saKey); + return saKey; + } else { + try { + String saKeyJson = readValueFromCredentialsFile(defaultCredentialsFilePath, EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY, EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY_PATH); + saKey = new Gson().fromJson(saKeyJson, ServiceAccountKey.class); + loadPrivateKey(cfg, saKey); + return saKey; + } catch (Exception e) { + throw new Exception("could not find service account key"); + } + } + } + + private void loadPrivateKey(Configuration cfg, ServiceAccountKey saKey) throws Exception { + if (!saKey.getCredentials().isPrivateKeySet()) { + try { + String privateKey = getPrivateKey(cfg); + saKey.getCredentials().setPrivateKey(privateKey); + } catch (Exception e) { + throw new Exception("could not find private key", e); + } + } + } + + private String getPrivateKey(Configuration cfg) throws Exception { + // Explicit code config + // Set private key + if (cfg.getPrivateKey() != null && !cfg.getPrivateKey().trim().isEmpty()) { + return cfg.getPrivateKey(); + } + // Set private key path + if (cfg.getPrivateKeyPath() != null && !cfg.getPrivateKeyPath().trim().isEmpty()) { + String privateKeyPath = cfg.getPrivateKeyPath(); + return new String(Files.readAllBytes(Paths.get(privateKeyPath)), StandardCharsets.UTF_8); + } + // Set credentials file + if (cfg.getCredentialsFilePath() != null && !cfg.getCredentialsFilePath().trim().isEmpty()) { + return readValueFromCredentialsFile(cfg.getCredentialsFilePath(), EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY, EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY_PATH); + } + + // ENVs config + if (EnvironmentVariables.STACKIT_PRIVATE_KEY != null && !EnvironmentVariables.STACKIT_PRIVATE_KEY.trim().isEmpty()) { + return EnvironmentVariables.STACKIT_PRIVATE_KEY.trim(); + } + if (EnvironmentVariables.STACKIT_PRIVATE_KEY_PATH != null && !EnvironmentVariables.STACKIT_PRIVATE_KEY_PATH.trim().isEmpty()) { + return new String(Files.readAllBytes(Paths.get(EnvironmentVariables.STACKIT_PRIVATE_KEY_PATH)), StandardCharsets.UTF_8); + } + if (EnvironmentVariables.STACKIT_CREDENTIALS_PATH != null && !EnvironmentVariables.STACKIT_CREDENTIALS_PATH.trim().isEmpty()) { + return readValueFromCredentialsFile(EnvironmentVariables.STACKIT_CREDENTIALS_PATH, EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY, EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY_PATH); + } + + // Read from credentials file in defaultCredentialsFilePath + return readValueFromCredentialsFile(defaultCredentialsFilePath, EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY, EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY_PATH); + } + + private String readValueFromCredentialsFile(String path, String valueKey, String pathKey) throws Exception { + // Read credentials file + String fileContent = new String(Files.readAllBytes(Paths.get(path)), StandardCharsets.UTF_8); + Type credentialsFileType = new TypeToken>(){}.getType(); + Map map = new Gson().fromJson(fileContent, credentialsFileType); + + // Read STACKIT_PRIVATE_KEY from credentials file + String privateKey = map.get(valueKey); + if (privateKey != null && !privateKey.trim().isEmpty()) { + return privateKey; + } + + // Read STACKIT_PRIVATE_KEY_PATH from credentials file + String privateKeyPath = map.get(pathKey); + if (privateKeyPath != null && !privateKeyPath.trim().isEmpty()) { + return new String(Files.readAllBytes(Paths.get(privateKeyPath))); + } + throw new Exception("could not find private key"); + } +} diff --git a/core/src/main/java/cloud/stackit/sdk/core/config/Configuration.java b/core/src/main/java/cloud/stackit/sdk/core/config/Configuration.java new file mode 100644 index 0000000..27d12e3 --- /dev/null +++ b/core/src/main/java/cloud/stackit/sdk/core/config/Configuration.java @@ -0,0 +1,124 @@ +package cloud.stackit.sdk.core.config; + +import java.util.Map; + +public class Configuration { + private final Map defaultHeader; + private final String serviceAccountKey; + private final String serviceAccountKeyPath; + private final String privateKeyPath; + private final String privateKey; + private final String customEndpoint; + private final String credentialsFilePath; + private final String tokenCustomUrl; + private final String tokenExpirationLeeway; + + Configuration(Builder builder) { + this.defaultHeader = builder.defaultHeader; + this.serviceAccountKey = builder.serviceAccountKey; + this.serviceAccountKeyPath = builder.serviceAccountKeyPath; + this.privateKeyPath = builder.privateKeyPath; + this.privateKey = builder.privateKey; + this.customEndpoint = builder.customEndpoint; + this.credentialsFilePath = builder.credentialsFilePath; + this.tokenCustomUrl = builder.tokenCustomUrl; + this.tokenExpirationLeeway = builder.tokenExpirationLeeway; + } + + public Map getDefaultHeader() { + return defaultHeader; + } + + public String getServiceAccountKey() { + return serviceAccountKey; + } + + public String getServiceAccountKeyPath() { + return serviceAccountKeyPath; + } + + public String getPrivateKeyPath() { + return privateKeyPath; + } + + public String getPrivateKey() { + return privateKey; + } + + public String getCustomEndpoint() { + return customEndpoint; + } + + public String getCredentialsFilePath() { + return credentialsFilePath; + } + + public String getTokenCustomUrl() { + return tokenCustomUrl; + } + + public String getTokenExpirationLeeway() { + return tokenExpirationLeeway; + } + + public static class Builder { + private Map defaultHeader; + private String serviceAccountKey; + private String serviceAccountKeyPath; + private String privateKeyPath; + private String privateKey; + private String customEndpoint; + private String credentialsFilePath; + private String tokenCustomUrl; + private String tokenExpirationLeeway; + + public Builder defaultHeader(Map defaultHeader) { + this.defaultHeader = defaultHeader; + return this; + } + + public Builder serviceAccountKey(String serviceAccountKey) { + this.serviceAccountKey = serviceAccountKey; + return this; + } + + public Builder serviceAccountKeyPath(String serviceAccountKeyPath) { + this.serviceAccountKeyPath = serviceAccountKeyPath; + return this; + } + + public Builder privateKeyPath(String privateKeyPath) { + this.privateKeyPath = privateKeyPath; + return this; + } + + public Builder privateKey(String privateKey) { + this.privateKey = privateKey; + return this; + } + + public Builder customEndpoint(String customEndpoint) { + this.customEndpoint = customEndpoint; + return this; + } + + public Builder credentialsFilePath(String credentialsFilePath) { + this.credentialsFilePath = credentialsFilePath; + return this; + } + + public Builder tokenCustomUrl(String tokenCustomUrl) { + this.tokenCustomUrl = tokenCustomUrl; + return this; + } + + public Builder tokenExpirationLeeway(String tokenExpirationLeeway) { + this.tokenExpirationLeeway = tokenExpirationLeeway; + return this; + } + + public Configuration build() { + return new Configuration(this); + } + } +} diff --git a/core/src/main/java/cloud/stackit/sdk/core/config/EnvironmentVariables.java b/core/src/main/java/cloud/stackit/sdk/core/config/EnvironmentVariables.java new file mode 100644 index 0000000..0d54c9f --- /dev/null +++ b/core/src/main/java/cloud/stackit/sdk/core/config/EnvironmentVariables.java @@ -0,0 +1,30 @@ +package cloud.stackit.sdk.core.config; + +public class EnvironmentVariables { + public final static String ENV_STACKIT_SERVICE_ACCOUNT_KEY_PATH = "STACKIT_SERVICE_ACCOUNT_KEY_PATH"; + public final static String ENV_STACKIT_SERVICE_ACCOUNT_KEY = "STACKIT_SERVICE_ACCOUNT_KEY"; + public final static String ENV_STACKIT_PRIVATE_KEY_PATH = "STACKIT_PRIVATE_KEY_PATH"; + public final static String ENV_STACKIT_PRIVATE_KEY = "STACKIT_PRIVATE_KEY"; + public final static String ENV_STACKIT_TOKEN_BASEURL = "STACKIT_TOKEN_BASEURL"; + public final static String ENV_STACKIT_CREDENTIALS_PATH = "STACKIT_CREDENTIALS_PATH"; + + public final static String STACKIT_SERVICE_ACCOUNT_KEY_PATH = System.getenv(ENV_STACKIT_SERVICE_ACCOUNT_KEY_PATH); + public final static String STACKIT_SERVICE_ACCOUNT_KEY = System.getenv(ENV_STACKIT_SERVICE_ACCOUNT_KEY); + public final static String STACKIT_PRIVATE_KEY_PATH = System.getenv(ENV_STACKIT_PRIVATE_KEY_PATH); + public final static String STACKIT_PRIVATE_KEY = System.getenv(ENV_STACKIT_PRIVATE_KEY); + public final static String STACKIT_TOKEN_BASEURL = System.getenv(ENV_STACKIT_TOKEN_BASEURL); + public final static String STACKIT_CREDENTIALS_PATH = System.getenv(ENV_STACKIT_CREDENTIALS_PATH); + + + @Override + public String toString() { + return "EnvironmentVariables{" + + "STACKIT_SERVICE_ACCOUNT_KEY_PATH='" + STACKIT_SERVICE_ACCOUNT_KEY_PATH + '\'' + + ", STACKIT_SERVICE_ACCOUNT_KEY='" + STACKIT_SERVICE_ACCOUNT_KEY + '\'' + + ", STACKIT_PRIVATE_KEY_PATH='" + STACKIT_PRIVATE_KEY_PATH + '\'' + + ", STACKIT_PRIVATE_KEY='" + STACKIT_PRIVATE_KEY + '\'' + + ", STACKIT_TOKEN_BASEURL='" + STACKIT_TOKEN_BASEURL + '\'' + + ", STACKIT_CREDENTIALS_PATH='" + STACKIT_CREDENTIALS_PATH + '\'' + + '}'; + } +} diff --git a/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountCredentials.java b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountCredentials.java index bdf42cc..e9803e0 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountCredentials.java +++ b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountCredentials.java @@ -2,7 +2,6 @@ import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; import java.security.interfaces.RSAPrivateKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; @@ -13,7 +12,7 @@ public class ServiceAccountCredentials { private final String aud; private final String iss; private final String kid; - private final String privateKey; + private String privateKey; private final UUID sub; public ServiceAccountCredentials(String aud, String iss, String kid, String privateKey, UUID sub) { @@ -40,6 +39,14 @@ public String getPrivateKey() { return privateKey; } + public void setPrivateKey(String privateKey) { + this.privateKey = privateKey; + } + + public boolean isPrivateKeySet() { + return !privateKey.trim().isEmpty(); + } + public UUID getSub() { return sub; } diff --git a/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java index 99d695d..0a9569f 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java +++ b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java @@ -2,12 +2,8 @@ import com.google.gson.Gson; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; import java.security.interfaces.RSAPublicKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; @@ -25,7 +21,6 @@ public class ServiceAccountKey { private final Date validUntil; private final ServiceAccountCredentials credentials; - public ServiceAccountKey(String id, String publicKey, Date created, String keyType, String keyOrigin, String keyAlgorithm, boolean active, Date validUntil, ServiceAccountCredentials credentials) { this.id = id; this.publicKey = publicKey; @@ -93,10 +88,7 @@ public RSAPublicKey getPublicKeyParsed() { return pubKey; } -// public static ServiceAccountKey loadCredentials(InputStream jsonStream) { -// return new Gson().fromJson(new InputStreamReader(jsonStream, StandardCharsets.UTF_8), ServiceAccountKey.class); -// } - public static ServiceAccountKey loadCredentials(String json) { + public static ServiceAccountKey loadCredentials(String json) throws com.google.gson.JsonSyntaxException { return new Gson().fromJson(json, ServiceAccountKey.class); } } From ffae87838551d152e6a275f46789347d13eab440 Mon Sep 17 00:00:00 2001 From: Marcel Jacek Date: Thu, 31 Jul 2025 17:22:23 +0200 Subject: [PATCH 03/12] improve documentation and error handling of KeyFlow and Configuration --- .../sdk/core/KeyFlowAuthenticator.java | 98 +++++++--- .../stackit/sdk/core/auth/SetupAuth.java | 183 +++++++++++++----- .../sdk/core/config/Configuration.java | 8 +- .../core/model/ServiceAccountCredentials.java | 26 +-- .../sdk/core/model/ServiceAccountKey.java | 38 ++-- 5 files changed, 244 insertions(+), 109 deletions(-) diff --git a/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java index ecd4c1f..7b6d728 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java +++ b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java @@ -1,9 +1,11 @@ package cloud.stackit.sdk.core; +import cloud.stackit.sdk.core.config.Configuration; import cloud.stackit.sdk.core.model.ServiceAccountKey; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; import com.google.gson.annotations.SerializedName; import okhttp3.*; @@ -11,22 +13,30 @@ import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; import java.security.interfaces.RSAPrivateKey; +import java.security.spec.InvalidKeySpecException; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; +/** + * KeyFlowAuthenticator handles the Key Flow Authentication based on the Service Account Key. + */ public class KeyFlowAuthenticator { private final String REFRESH_TOKEN = "refresh_token"; private final String ASSERTION = "assertion"; + private final String DEFAULT_TOKEN_ENDPOINT = "https://service-account.api.stackit.cloud/token"; + private final long DEFAULT_TOKEN_LEEWAY = 60; private final OkHttpClient httpClient; private final ServiceAccountKey saKey; private KeyFlowTokenResponse token; private final Gson gson; private final String tokenUrl; + private long tokenLeewayInSeconds = DEFAULT_TOKEN_LEEWAY; private static class KeyFlowTokenResponse { @SerializedName("access_token") @@ -41,7 +51,7 @@ private static class KeyFlowTokenResponse { private String tokenType; public boolean isExpired() { - return expiresIn < new Date().toInstant().minusSeconds(60).getEpochSecond(); + return expiresIn < new Date().toInstant().getEpochSecond(); } public String getAccessToken() { @@ -49,7 +59,14 @@ public String getAccessToken() { } } - public KeyFlowAuthenticator(ServiceAccountKey saKey) { + /** + * Creates the initial service account and refreshes expired access token. + * @param cfg Configuration to set a custom token endpoint and the token expiration leeway. + * @param saKey Service Account Key, which should be used for the authentication + * @throws InvalidKeySpecException Throws, when the private key in the service account can not be parsed + * @throws IOException Throws, when on unexpected responses from the key flow + */ + public KeyFlowAuthenticator(Configuration cfg, ServiceAccountKey saKey) throws InvalidKeySpecException, IOException { this.saKey = saKey; this.gson = new Gson(); this.httpClient = new OkHttpClient.Builder() @@ -57,7 +74,15 @@ public KeyFlowAuthenticator(ServiceAccountKey saKey) { .writeTimeout(10, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build(); - this.tokenUrl = "https://service-account.api.stackit.cloud/token"; + if (cfg.getTokenCustomUrl() != null && !cfg.getTokenCustomUrl().trim().isEmpty()) { + this.tokenUrl = cfg.getTokenCustomUrl(); + } else { + this.tokenUrl = DEFAULT_TOKEN_ENDPOINT; + } + if (cfg.getTokenExpirationLeeway() != null && cfg.getTokenExpirationLeeway() > 0) { + this.tokenLeewayInSeconds = cfg.getTokenExpirationLeeway(); + } + createAccessToken(); } @@ -68,26 +93,46 @@ public synchronized String getAccessToken() throws IOException { return token.getAccessToken(); } - private void createAccessToken() { + /** + * Creates the inital accessToken and stores it in `this.token` + * @throws InvalidKeySpecException can not parse private key + * @throws IOException request for access token failed + * @throws JsonSyntaxException parsing of the created access token failed + */ + private void createAccessToken() throws InvalidKeySpecException, IOException, JsonSyntaxException { String grant = "urn:ietf:params:oauth:grant-type:jwt-bearer"; - String assertion = generateSelfSignedJWT(); + String assertion; + try { + assertion = generateSelfSignedJWT(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("could not find required algorithm for jwt signing. This should not happen and should be reported on https://github.com/stackitcloud/stackit-sdk-java/issues", e); + } try(Response response = requestToken(grant, assertion).execute()) { parseTokenResponse(response); } catch (IOException | ApiException e) { - e.printStackTrace(); + throw new IOException("request for access token failed", e); + } catch (JsonSyntaxException e) { + throw new JsonSyntaxException("parsing access token failed", e); } } - private synchronized void createAccessTokenWithRefreshToken() throws IOException { + /** + * Creates a new access token with the existing refresh token + * @throws IOException request for new access token failed + * @throws JsonSyntaxException can not parse new access token + */ + private synchronized void createAccessTokenWithRefreshToken() throws IOException, JsonSyntaxException { String refreshToken = token.refreshToken; try (Response response = requestToken(REFRESH_TOKEN, refreshToken).execute()) { parseTokenResponse(response); - } catch (ApiException e) { - e.printStackTrace(); + } catch (IOException | ApiException e) { + throw new IOException("request for new access token failed", e); + } catch (JsonSyntaxException e) { + throw new JsonSyntaxException("parsing refreshed access token failed", e); } } - private synchronized void parseTokenResponse(Response response) throws ApiException { + private synchronized void parseTokenResponse(Response response) throws ApiException, JsonSyntaxException { if (response.code() != HttpURLConnection.HTTP_OK) { String body = null; if (response.body() != null) { @@ -97,22 +142,23 @@ private synchronized void parseTokenResponse(Response response) throws ApiExcept throw new ApiException(response.message(), response.code(), response.headers().toMultimap(), body); } if (response.body() == null) { - throw new ApiException("body from token creation is null"); + throw new JsonSyntaxException("body from token creation is null"); } - token = gson.fromJson(new InputStreamReader(response.body().byteStream(), StandardCharsets.UTF_8), KeyFlowTokenResponse.class); - token.expiresIn = JWT.decode(token.accessToken).getExpiresAt().toInstant().getEpochSecond(); - response.body().close(); + try { + token = gson.fromJson(new InputStreamReader(response.body().byteStream(), StandardCharsets.UTF_8), KeyFlowTokenResponse.class); + token.expiresIn = JWT.decode(token.accessToken).getExpiresAt().toInstant().minusSeconds(tokenLeewayInSeconds).getEpochSecond(); + response.body().close(); + } catch (JsonSyntaxException e) { + throw new JsonSyntaxException("could not parse response of created token", e); + } } - private Call requestToken(String grant, String assertion) throws IOException { + private Call requestToken(String grant, String assertionValue) throws IOException { FormBody.Builder bodyBuilder = new FormBody.Builder(); bodyBuilder.addEncoded("grant_type", grant); - if (grant.equals(REFRESH_TOKEN)) { - bodyBuilder.addEncoded(REFRESH_TOKEN, assertion); - } else { - bodyBuilder.addEncoded(ASSERTION, assertion); - } + String assertionKey = grant.equals(REFRESH_TOKEN) ? REFRESH_TOKEN : ASSERTION; + bodyBuilder.addEncoded(assertionKey, assertionValue); FormBody body = bodyBuilder.build(); Request request = new Request.Builder() @@ -123,14 +169,14 @@ private Call requestToken(String grant, String assertion) throws IOException { return httpClient.newCall(request); } - private String generateSelfSignedJWT() { - RSAPrivateKey prvKey = saKey.getCredentials().getPrivateKeyParsed(); - Algorithm algorithm = null; + private String generateSelfSignedJWT() throws InvalidKeySpecException, NoSuchAlgorithmException { + RSAPrivateKey prvKey; try { - algorithm = Algorithm.RSA512(prvKey); - } catch (Exception e) { - e.printStackTrace(); + prvKey = saKey.getCredentials().getPrivateKeyParsed(); + } catch (InvalidKeySpecException e) { + throw new InvalidKeySpecException("could not parse private key", e); } + Algorithm algorithm = Algorithm.RSA512(prvKey); Map jwtHeader = new HashMap<>(); jwtHeader.put("kid", saKey.getCredentials().getKid()); diff --git a/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java b/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java index d1b1d08..d3abc4f 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java +++ b/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java @@ -9,96 +9,176 @@ import com.google.gson.reflect.TypeToken; import okhttp3.Interceptor; +import javax.security.auth.login.CredentialNotFoundException; +import javax.swing.filechooser.FileSystemView; +import java.io.File; +import java.io.IOException; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; +import java.security.spec.InvalidKeySpecException; import java.util.Map; public class SetupAuth { - private Interceptor authHandler; - private final String defaultCredentialsFilePath = "~/.stackit/credentials.json"; - - public SetupAuth() { + private final Interceptor authHandler; + private final String defaultCredentialsFilePath = + FileSystemView.getFileSystemView().getHomeDirectory() + + File.separator + + ".stackit" + + File.separator + + "credentials.json"; + + /** + * Set up the KeyFlow Authentication and can be integrated in an OkHttp client, by adding `SetupAuth().getAuthHandler()` as interceptor. + * This relies on the configuration methods via ENVs or the credentials file in `$HOME/.stackit/credentials.json` + * @throws IOException when no file can be found + * @throws CredentialNotFoundException when no configuration is set or can be found + * @throws InvalidKeySpecException when the private key can not be parsed + */ + public SetupAuth() throws IOException, InvalidKeySpecException, CredentialNotFoundException { this(new Configuration.Builder().build()); } - public SetupAuth(Configuration cfg) { + /** + * Set up the KeyFlow Authentication and can be integrated in an OkHttp client, by adding `SetupAuth().getAuthHandler()` as interceptor. + * @param cfg Configuration which describes, which service account and token endpoint should be used + * @throws IOException when no file can be found + * @throws CredentialNotFoundException when no configuration is set or can be found + * @throws InvalidKeySpecException when the private key can not be parsed + */ + public SetupAuth(Configuration cfg) throws IOException, CredentialNotFoundException, InvalidKeySpecException { if (cfg == null) { cfg = new Configuration.Builder().build(); } - try { - ServiceAccountKey saKey = setupKeyFlow(cfg); - authHandler = new KeyFlowInterceptor(new KeyFlowAuthenticator(saKey)); - } catch (Exception e) { - e.printStackTrace(); - } + ServiceAccountKey saKey = setupKeyFlow(cfg); + authHandler = new KeyFlowInterceptor(new KeyFlowAuthenticator(cfg, saKey)); } public Interceptor getAuthHandler() { return authHandler; } - private ServiceAccountKey setupKeyFlow(Configuration cfg) throws Exception { - ServiceAccountKey saKey = null; + /** + * setupKeyFlow return first found ServiceAccountKey + * Reads the configured options in the following order + *
    + *
  1. + * Explicit configuration in `Configuration` + *
  2. + *
      + *
    • serviceAccountKey
    • + *
    • serviceAccountKeyPath
    • + *
    • credentialsFilePath -> STACKIT_SERVICE_ACCOUNT_KEY / STACKIT_SERVICE_ACCOUNT_KEY_PATH
    • + *
    + *
  3. + * Environment variables + *
  4. + *
      + *
    • STACKIT_SERVICE_ACCOUNT_KEY
    • + *
    • STACKIT_SERVICE_ACCOUNT_KEY_PATH
    • + *
    • STACKIT_CREDENTIALS_PATH -> STACKIT_SERVICE_ACCOUNT_KEY / STACKIT_SERVICE_ACCOUNT_KEY_PATH
    • + *
    + *
  5. + * Credentials file + *
  6. + *
      + *
    • STACKIT_SERVICE_ACCOUNT_KEY
    • + *
    • STACKIT_SERVICE_ACCOUNT_KEY_PATH
    • + *
    + *
+ * @param cfg + * @return ServiceAccountKey + * @throws CredentialNotFoundException throws error when no service account key or private key can be found + * @throws IOException throws an error if a file can not be found + */ + private ServiceAccountKey setupKeyFlow(Configuration cfg) throws CredentialNotFoundException, IOException { // Explicit config in code if (cfg.getServiceAccountKey() != null && !cfg.getServiceAccountKey().trim().isEmpty()) { - saKey = ServiceAccountKey.loadCredentials(cfg.getServiceAccountKey()); + ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(cfg.getServiceAccountKey()); loadPrivateKey(cfg, saKey); return saKey; } if (cfg.getServiceAccountKeyPath() != null && !cfg.getServiceAccountKeyPath().trim().isEmpty()) { String fileContent = new String(Files.readAllBytes(Paths.get(cfg.getServiceAccountKeyPath())), StandardCharsets.UTF_8); - saKey = new Gson().fromJson(fileContent, ServiceAccountKey.class); + ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(fileContent); loadPrivateKey(cfg, saKey); return saKey; } // Env config - if (!EnvironmentVariables.STACKIT_SERVICE_ACCOUNT_KEY.trim().isEmpty()) { - saKey = ServiceAccountKey.loadCredentials(EnvironmentVariables.STACKIT_SERVICE_ACCOUNT_KEY.trim()); + if (EnvironmentVariables.STACKIT_SERVICE_ACCOUNT_KEY != null && !EnvironmentVariables.STACKIT_SERVICE_ACCOUNT_KEY.trim().isEmpty()) { + ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(EnvironmentVariables.STACKIT_SERVICE_ACCOUNT_KEY.trim()); loadPrivateKey(cfg, saKey); return saKey; } - if (!EnvironmentVariables.STACKIT_SERVICE_ACCOUNT_KEY_PATH.trim().isEmpty()) { + if (EnvironmentVariables.STACKIT_SERVICE_ACCOUNT_KEY_PATH != null && !EnvironmentVariables.STACKIT_SERVICE_ACCOUNT_KEY_PATH.trim().isEmpty()) { String fileContent = new String(Files.readAllBytes(Paths.get(cfg.getServiceAccountKeyPath())), StandardCharsets.UTF_8); - saKey = new Gson().fromJson(fileContent, ServiceAccountKey.class); + ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(fileContent); loadPrivateKey(cfg, saKey); return saKey; } - if (!EnvironmentVariables.STACKIT_CREDENTIALS_PATH.trim().isEmpty()) { + if (EnvironmentVariables.STACKIT_CREDENTIALS_PATH != null && !EnvironmentVariables.STACKIT_CREDENTIALS_PATH.trim().isEmpty()) { String saKeyJson = readValueFromCredentialsFile(EnvironmentVariables.STACKIT_CREDENTIALS_PATH, EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY, EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY_PATH); - saKey = new Gson().fromJson(saKeyJson, ServiceAccountKey.class); + ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(saKeyJson); loadPrivateKey(cfg, saKey); return saKey; } else { - try { - String saKeyJson = readValueFromCredentialsFile(defaultCredentialsFilePath, EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY, EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY_PATH); - saKey = new Gson().fromJson(saKeyJson, ServiceAccountKey.class); - loadPrivateKey(cfg, saKey); - return saKey; - } catch (Exception e) { - throw new Exception("could not find service account key"); - } + String saKeyJson = readValueFromCredentialsFile(defaultCredentialsFilePath, EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY, EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY_PATH); + ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(saKeyJson); + loadPrivateKey(cfg, saKey); + return saKey; } } - private void loadPrivateKey(Configuration cfg, ServiceAccountKey saKey) throws Exception { + private void loadPrivateKey(Configuration cfg, ServiceAccountKey saKey) throws CredentialNotFoundException { if (!saKey.getCredentials().isPrivateKeySet()) { try { String privateKey = getPrivateKey(cfg); saKey.getCredentials().setPrivateKey(privateKey); } catch (Exception e) { - throw new Exception("could not find private key", e); + throw new CredentialNotFoundException("could not find private key\n" + e.getMessage()); } } } - private String getPrivateKey(Configuration cfg) throws Exception { + /** + * Reads the private key in the following order + *
    + *
  1. + * Explicit configuration in `Configuration` + *
  2. + *
      + *
    • privateKey
    • + *
    • privateKeyPath
    • + *
    • credentialsFilePath -> STACKIT_PRIVATE_KEY / STACKIT_PRIVATE_KEY_PATH
    • + *
    + *
  3. + * Environment variables + *
  4. + *
      + *
    • STACKIT_PRIVATE_KEY
    • + *
    • STACKIT_PRIVATE_KEY_PATH
    • + *
    • STACKIT_CREDENTIALS_PATH -> STACKIT_PRIVATE_KEY / STACKIT_PRIVATE_KEY_PATH
    • + *
    + *
  5. + * Credentials file + *
  6. + *
      + *
    • STACKIT_PRIVATE_KEY
    • + *
    • STACKIT_PRIVATE_KEY_PATH
    • + *
    + *
+ * @param cfg + * @return found private key + * @throws CredentialNotFoundException throws if no private key could be found + * @throws IOException throws if the provided path can not be found or the file within the pathKey can not be found + */ + private String getPrivateKey(Configuration cfg) throws CredentialNotFoundException, IOException { // Explicit code config // Set private key if (cfg.getPrivateKey() != null && !cfg.getPrivateKey().trim().isEmpty()) { @@ -129,23 +209,38 @@ private String getPrivateKey(Configuration cfg) throws Exception { return readValueFromCredentialsFile(defaultCredentialsFilePath, EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY, EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY_PATH); } - private String readValueFromCredentialsFile(String path, String valueKey, String pathKey) throws Exception { + /** + * Reads of a json credentials file from `path`, the values of `valueKey` or `pathKey`. + * @param path Path of the credentials file which should be read + * @param valueKey key which contains the secret as value + * @param pathKey key which contains a path to a file + * @return Either the value of `valueKey` or the content of the file in `pathKey` + * @throws CredentialNotFoundException throws if no value was found in the credentials file + * @throws IOException throws if the provided path can not be found or the file within the pathKey can not be found + */ + private String readValueFromCredentialsFile(String path, String valueKey, String pathKey) throws IOException, CredentialNotFoundException { // Read credentials file String fileContent = new String(Files.readAllBytes(Paths.get(path)), StandardCharsets.UTF_8); - Type credentialsFileType = new TypeToken>(){}.getType(); - Map map = new Gson().fromJson(fileContent, credentialsFileType); + Type credentialsFileType = new TypeToken>(){}.getType(); + Map map = new Gson().fromJson(fileContent, credentialsFileType); - // Read STACKIT_PRIVATE_KEY from credentials file - String privateKey = map.get(valueKey); - if (privateKey != null && !privateKey.trim().isEmpty()) { - return privateKey; + // Read KEY from credentials file + String key = null; + try { + key = (String) map.get(valueKey); + } catch (ClassCastException ignored) {} + if (key != null && !key.trim().isEmpty()) { + return key; } - // Read STACKIT_PRIVATE_KEY_PATH from credentials file - String privateKeyPath = map.get(pathKey); - if (privateKeyPath != null && !privateKeyPath.trim().isEmpty()) { - return new String(Files.readAllBytes(Paths.get(privateKeyPath))); + // Read KEY_PATH from credentials file + String keyPath = null; + try { + keyPath = (String) map.get(pathKey); + } catch (ClassCastException ignored) {} + if (keyPath != null && !keyPath.trim().isEmpty()) { + return new String(Files.readAllBytes(Paths.get(keyPath))); } - throw new Exception("could not find private key"); + throw new CredentialNotFoundException("could not find " + valueKey + " or " + pathKey + " in " + path); } } diff --git a/core/src/main/java/cloud/stackit/sdk/core/config/Configuration.java b/core/src/main/java/cloud/stackit/sdk/core/config/Configuration.java index 27d12e3..705d29c 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/config/Configuration.java +++ b/core/src/main/java/cloud/stackit/sdk/core/config/Configuration.java @@ -11,7 +11,7 @@ public class Configuration { private final String customEndpoint; private final String credentialsFilePath; private final String tokenCustomUrl; - private final String tokenExpirationLeeway; + private final Long tokenExpirationLeeway; Configuration(Builder builder) { this.defaultHeader = builder.defaultHeader; @@ -57,7 +57,7 @@ public String getTokenCustomUrl() { return tokenCustomUrl; } - public String getTokenExpirationLeeway() { + public Long getTokenExpirationLeeway() { return tokenExpirationLeeway; } @@ -70,7 +70,7 @@ public static class Builder { private String customEndpoint; private String credentialsFilePath; private String tokenCustomUrl; - private String tokenExpirationLeeway; + private Long tokenExpirationLeeway; public Builder defaultHeader(Map defaultHeader) { this.defaultHeader = defaultHeader; @@ -112,7 +112,7 @@ public Builder tokenCustomUrl(String tokenCustomUrl) { return this; } - public Builder tokenExpirationLeeway(String tokenExpirationLeeway) { + public Builder tokenExpirationLeeway(Long tokenExpirationLeeway) { this.tokenExpirationLeeway = tokenExpirationLeeway; return this; } diff --git a/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountCredentials.java b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountCredentials.java index e9803e0..8778d99 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountCredentials.java +++ b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountCredentials.java @@ -44,29 +44,21 @@ public void setPrivateKey(String privateKey) { } public boolean isPrivateKeySet() { - return !privateKey.trim().isEmpty(); + return privateKey != null && !privateKey.trim().isEmpty(); } public UUID getSub() { return sub; } - public RSAPrivateKey getPrivateKeyParsed() { - RSAPrivateKey prvKey = null; - try { - String trimmedKey = privateKey.replaceFirst("-----BEGIN PRIVATE KEY-----", ""); - trimmedKey = trimmedKey.replaceFirst("-----END PRIVATE KEY-----", ""); - trimmedKey = trimmedKey.replaceAll("\n",""); + public RSAPrivateKey getPrivateKeyParsed() throws NoSuchAlgorithmException, InvalidKeySpecException { + String trimmedKey = privateKey.replaceFirst("-----BEGIN PRIVATE KEY-----", ""); + trimmedKey = trimmedKey.replaceFirst("-----END PRIVATE KEY-----", ""); + trimmedKey = trimmedKey.replaceAll("\n",""); - byte[] privateBytes = Base64.getDecoder().decode(trimmedKey); - PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateBytes); - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - prvKey = (RSAPrivateKey) keyFactory.generatePrivate(keySpec); - } catch (InvalidKeySpecException e) { - e.printStackTrace(); - } catch (NoSuchAlgorithmException e) { - System.out.println(e); - } - return prvKey; + byte[] privateBytes = Base64.getDecoder().decode(trimmedKey); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + return (RSAPrivateKey) keyFactory.generatePrivate(keySpec); } } diff --git a/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java index 0a9569f..9ce17af 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java +++ b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java @@ -1,6 +1,7 @@ package cloud.stackit.sdk.core.model; import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; @@ -69,26 +70,27 @@ public ServiceAccountCredentials getCredentials() { return credentials; } - public RSAPublicKey getPublicKeyParsed() { - RSAPublicKey pubKey = null; - try { - String trimmedKey = publicKey.replaceFirst("-----BEGIN PUBLIC KEY-----", ""); - trimmedKey = trimmedKey.replaceFirst("-----END PUBLIC KEY-----", ""); - trimmedKey = trimmedKey.replaceAll("\n",""); - - byte[] publicBytes = Base64.getDecoder().decode(trimmedKey); - X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicBytes); - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - pubKey = (RSAPublicKey) keyFactory.generatePublic(keySpec); - } catch (InvalidKeySpecException e) { - e.printStackTrace(); - } catch (NoSuchAlgorithmException e) { - System.out.println(e); - } + public RSAPublicKey getPublicKeyParsed() throws NoSuchAlgorithmException, InvalidKeySpecException { + String trimmedKey = publicKey.replaceFirst("-----BEGIN PUBLIC KEY-----", ""); + trimmedKey = trimmedKey.replaceFirst("-----END PUBLIC KEY-----", ""); + trimmedKey = trimmedKey.replaceAll("\n",""); + + byte[] publicBytes = Base64.getDecoder().decode(trimmedKey); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + RSAPublicKey pubKey = (RSAPublicKey) keyFactory.generatePublic(keySpec); return pubKey; } - public static ServiceAccountKey loadCredentials(String json) throws com.google.gson.JsonSyntaxException { - return new Gson().fromJson(json, ServiceAccountKey.class); + public static ServiceAccountKey loadFromJson(String json) throws com.google.gson.JsonSyntaxException { + ServiceAccountKey saKey = new Gson().fromJson(json, ServiceAccountKey.class); + if (!saKey.isCredentialsSet()) { + throw new JsonSyntaxException("required field `credentials` in service account key is missing."); + } + return saKey; + } + + private boolean isCredentialsSet() { + return credentials != null; } } From 7e60121bb8833879058f5440de27b40e790eed29 Mon Sep 17 00:00:00 2001 From: Marcel Jacek Date: Thu, 31 Jul 2025 17:37:26 +0200 Subject: [PATCH 04/12] Adjust resource manager to use CoreConfiguration for service account --- .../sdk/core/KeyFlowAuthenticator.java | 4 +-- .../stackit/sdk/core/auth/SetupAuth.java | 14 +++++----- ...figuration.java => CoreConfiguration.java} | 8 +++--- .../sdk/resourcemanager/ApiClient.java | 22 +++++++++++++--- .../sdk/resourcemanager/api/DefaultApi.java | 19 +++++++++++--- .../stackit/sdk/resourcemanager/main.java | 26 +++++++++++++++++++ 6 files changed, 74 insertions(+), 19 deletions(-) rename core/src/main/java/cloud/stackit/sdk/core/config/{Configuration.java => CoreConfiguration.java} (95%) create mode 100644 services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/main.java diff --git a/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java index 7b6d728..fb55208 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java +++ b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java @@ -1,6 +1,6 @@ package cloud.stackit.sdk.core; -import cloud.stackit.sdk.core.config.Configuration; +import cloud.stackit.sdk.core.config.CoreConfiguration; import cloud.stackit.sdk.core.model.ServiceAccountKey; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; @@ -66,7 +66,7 @@ public String getAccessToken() { * @throws InvalidKeySpecException Throws, when the private key in the service account can not be parsed * @throws IOException Throws, when on unexpected responses from the key flow */ - public KeyFlowAuthenticator(Configuration cfg, ServiceAccountKey saKey) throws InvalidKeySpecException, IOException { + public KeyFlowAuthenticator(CoreConfiguration cfg, ServiceAccountKey saKey) throws InvalidKeySpecException, IOException { this.saKey = saKey; this.gson = new Gson(); this.httpClient = new OkHttpClient.Builder() diff --git a/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java b/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java index d3abc4f..308a1a3 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java +++ b/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java @@ -1,7 +1,7 @@ package cloud.stackit.sdk.core.auth; import cloud.stackit.sdk.core.KeyFlowAuthenticator; -import cloud.stackit.sdk.core.config.Configuration; +import cloud.stackit.sdk.core.config.CoreConfiguration; import cloud.stackit.sdk.core.config.EnvironmentVariables; import cloud.stackit.sdk.core.KeyFlowInterceptor; import cloud.stackit.sdk.core.model.ServiceAccountKey; @@ -37,7 +37,7 @@ public class SetupAuth { * @throws InvalidKeySpecException when the private key can not be parsed */ public SetupAuth() throws IOException, InvalidKeySpecException, CredentialNotFoundException { - this(new Configuration.Builder().build()); + this(new CoreConfiguration.Builder().build()); } /** @@ -47,9 +47,9 @@ public SetupAuth() throws IOException, InvalidKeySpecException, CredentialNotFou * @throws CredentialNotFoundException when no configuration is set or can be found * @throws InvalidKeySpecException when the private key can not be parsed */ - public SetupAuth(Configuration cfg) throws IOException, CredentialNotFoundException, InvalidKeySpecException { + public SetupAuth(CoreConfiguration cfg) throws IOException, CredentialNotFoundException, InvalidKeySpecException { if (cfg == null) { - cfg = new Configuration.Builder().build(); + cfg = new CoreConfiguration.Builder().build(); } ServiceAccountKey saKey = setupKeyFlow(cfg); @@ -93,7 +93,7 @@ public Interceptor getAuthHandler() { * @throws CredentialNotFoundException throws error when no service account key or private key can be found * @throws IOException throws an error if a file can not be found */ - private ServiceAccountKey setupKeyFlow(Configuration cfg) throws CredentialNotFoundException, IOException { + private ServiceAccountKey setupKeyFlow(CoreConfiguration cfg) throws CredentialNotFoundException, IOException { // Explicit config in code if (cfg.getServiceAccountKey() != null && !cfg.getServiceAccountKey().trim().isEmpty()) { ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(cfg.getServiceAccountKey()); @@ -135,7 +135,7 @@ private ServiceAccountKey setupKeyFlow(Configuration cfg) throws CredentialNotFo } } - private void loadPrivateKey(Configuration cfg, ServiceAccountKey saKey) throws CredentialNotFoundException { + private void loadPrivateKey(CoreConfiguration cfg, ServiceAccountKey saKey) throws CredentialNotFoundException { if (!saKey.getCredentials().isPrivateKeySet()) { try { String privateKey = getPrivateKey(cfg); @@ -178,7 +178,7 @@ private void loadPrivateKey(Configuration cfg, ServiceAccountKey saKey) throws C * @throws CredentialNotFoundException throws if no private key could be found * @throws IOException throws if the provided path can not be found or the file within the pathKey can not be found */ - private String getPrivateKey(Configuration cfg) throws CredentialNotFoundException, IOException { + private String getPrivateKey(CoreConfiguration cfg) throws CredentialNotFoundException, IOException { // Explicit code config // Set private key if (cfg.getPrivateKey() != null && !cfg.getPrivateKey().trim().isEmpty()) { diff --git a/core/src/main/java/cloud/stackit/sdk/core/config/Configuration.java b/core/src/main/java/cloud/stackit/sdk/core/config/CoreConfiguration.java similarity index 95% rename from core/src/main/java/cloud/stackit/sdk/core/config/Configuration.java rename to core/src/main/java/cloud/stackit/sdk/core/config/CoreConfiguration.java index 705d29c..2579ed8 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/config/Configuration.java +++ b/core/src/main/java/cloud/stackit/sdk/core/config/CoreConfiguration.java @@ -2,7 +2,7 @@ import java.util.Map; -public class Configuration { +public class CoreConfiguration { private final Map defaultHeader; private final String serviceAccountKey; private final String serviceAccountKeyPath; @@ -13,7 +13,7 @@ public class Configuration { private final String tokenCustomUrl; private final Long tokenExpirationLeeway; - Configuration(Builder builder) { + CoreConfiguration(Builder builder) { this.defaultHeader = builder.defaultHeader; this.serviceAccountKey = builder.serviceAccountKey; this.serviceAccountKeyPath = builder.serviceAccountKeyPath; @@ -117,8 +117,8 @@ public Builder tokenExpirationLeeway(Long tokenExpirationLeeway) { return this; } - public Configuration build() { - return new Configuration(this); + public CoreConfiguration build() { + return new CoreConfiguration(this); } } } diff --git a/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/ApiClient.java b/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/ApiClient.java index 7170f77..66eff18 100644 --- a/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/ApiClient.java +++ b/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/ApiClient.java @@ -118,9 +118,25 @@ public ApiClient(OkHttpClient client) { authentications = Collections.unmodifiableMap(authentications); } - protected void initHttpClient() { - initHttpClient(Collections.emptyList()); - } + public ApiClient(CoreConfiguration config) throws IOException, InvalidKeySpecException, CredentialNotFoundException { + init(); + + if (config.getCustomEndpoint() != null && !config.getCustomEndpoint().trim().isEmpty()) { + basePath = config.getCustomEndpoint(); + } + if (config.getDefaultHeader() != null) { + defaultHeaderMap = config.getDefaultHeader(); + } + SetupAuth auth; + auth = new SetupAuth(config); + List interceptors = new LinkedList<>(); + interceptors.add(auth.getAuthHandler()); + initHttpClient(interceptors); + } + + protected void initHttpClient() { + initHttpClient(Collections.emptyList()); + } protected void initHttpClient(List interceptors) { OkHttpClient.Builder builder = new OkHttpClient.Builder(); diff --git a/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/api/DefaultApi.java b/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/api/DefaultApi.java index 0bf04ea..07938ec 100644 --- a/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/api/DefaultApi.java +++ b/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/api/DefaultApi.java @@ -12,6 +12,7 @@ package cloud.stackit.sdk.resourcemanager.api; +import cloud.stackit.sdk.core.config.CoreConfiguration; import cloud.stackit.sdk.resourcemanager.ApiCallback; import cloud.stackit.sdk.resourcemanager.ApiClient; import cloud.stackit.sdk.resourcemanager.ApiException; @@ -26,12 +27,17 @@ import cloud.stackit.sdk.resourcemanager.model.ListFoldersResponse; import cloud.stackit.sdk.resourcemanager.model.ListOrganizationsResponse; import cloud.stackit.sdk.resourcemanager.model.ListProjectsResponse; + +import java.security.spec.InvalidKeySpecException; +import java.time.OffsetDateTime; import cloud.stackit.sdk.resourcemanager.model.OrganizationResponse; import cloud.stackit.sdk.resourcemanager.model.PartialUpdateFolderPayload; import cloud.stackit.sdk.resourcemanager.model.PartialUpdateOrganizationPayload; import cloud.stackit.sdk.resourcemanager.model.PartialUpdateProjectPayload; import cloud.stackit.sdk.resourcemanager.model.Project; import com.google.gson.reflect.TypeToken; + +import javax.security.auth.login.CredentialNotFoundException; import java.lang.reflect.Type; import java.math.BigDecimal; import java.time.OffsetDateTime; @@ -53,9 +59,16 @@ public DefaultApi(ApiClient apiClient) { this.localVarApiClient = apiClient; } - public ApiClient getApiClient() { - return localVarApiClient; - } + public DefaultApi(CoreConfiguration config) throws IOException, InvalidKeySpecException, CredentialNotFoundException { + if (config.getCustomEndpoint() != null && !config.getCustomEndpoint().trim().isEmpty()) { + localCustomBaseUrl = config.getCustomEndpoint(); + } + this.localVarApiClient = new ApiClient(config); + } + + public ApiClient getApiClient() { + return localVarApiClient; + } public void setApiClient(ApiClient apiClient) { this.localVarApiClient = apiClient; diff --git a/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/main.java b/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/main.java new file mode 100644 index 0000000..9ca5939 --- /dev/null +++ b/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/main.java @@ -0,0 +1,26 @@ +package cloud.stackit.sdk.resourcemanager; + +import cloud.stackit.sdk.core.config.CoreConfiguration; +import cloud.stackit.sdk.resourcemanager.api.DefaultApi; +import cloud.stackit.sdk.resourcemanager.model.ListOrganizationsResponse; + +import javax.security.auth.login.CredentialNotFoundException; +import java.io.IOException; +import java.security.spec.InvalidKeySpecException; + +public class main { + public static void main(String[] args) { + String SERVICE_ACCOUNT_KEY_PATH = "/path/to/your/sa/key.json"; + String SERIVCE_ACCOUNT_MAIL = "name-1234@sa.stackit.cloud"; + + CoreConfiguration config = new CoreConfiguration + .Builder() + .serviceAccountKeyPath(SERVICE_ACCOUNT_KEY_PATH) + .build(); + DefaultApi api = new DefaultApi(config); + + ListOrganizationsResponse response = api.listOrganizations(null, SERIVCE_ACCOUNT_MAIL, null, null, null); + + System.out.println(response); + } +} From bbb974f911ea562a6a263e66d7bf5335a2f2820f Mon Sep 17 00:00:00 2001 From: Marcel Jacek Date: Fri, 1 Aug 2025 15:23:11 +0200 Subject: [PATCH 05/12] review feedback removed try-catch-blocks --- .../sdk/core/KeyFlowAuthenticator.java | 61 +++++++++---------- .../stackit/sdk/core/KeyFlowInterceptor.java | 10 ++- .../stackit/sdk/core/auth/SetupAuth.java | 35 ++++++----- .../core/{ => exception}/ApiException.java | 2 +- .../CredentialsInFileNotFoundException.java | 12 ++++ .../PrivateKeyNotFoundException.java | 12 ++++ .../sdk/core/model/ServiceAccountKey.java | 5 +- .../sdk/resourcemanager/ApiClient.java | 2 +- .../sdk/resourcemanager/api/DefaultApi.java | 3 +- .../stackit/sdk/resourcemanager/main.java | 2 +- 10 files changed, 88 insertions(+), 56 deletions(-) rename core/src/main/java/cloud/stackit/sdk/core/{ => exception}/ApiException.java (99%) create mode 100644 core/src/main/java/cloud/stackit/sdk/core/exception/CredentialsInFileNotFoundException.java create mode 100644 core/src/main/java/cloud/stackit/sdk/core/exception/PrivateKeyNotFoundException.java diff --git a/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java index fb55208..44abf93 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java +++ b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java @@ -1,6 +1,7 @@ package cloud.stackit.sdk.core; import cloud.stackit.sdk.core.config.CoreConfiguration; +import cloud.stackit.sdk.core.exception.ApiException; import cloud.stackit.sdk.core.model.ServiceAccountKey; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; @@ -63,10 +64,11 @@ public String getAccessToken() { * Creates the initial service account and refreshes expired access token. * @param cfg Configuration to set a custom token endpoint and the token expiration leeway. * @param saKey Service Account Key, which should be used for the authentication - * @throws InvalidKeySpecException Throws, when the private key in the service account can not be parsed - * @throws IOException Throws, when on unexpected responses from the key flow + * @throws InvalidKeySpecException thrown when the private key in the service account can not be parsed + * @throws IOException thrown on unexpected responses from the key flow + * @throws ApiException thrown on unexpected responses from the key flow */ - public KeyFlowAuthenticator(CoreConfiguration cfg, ServiceAccountKey saKey) throws InvalidKeySpecException, IOException { + public KeyFlowAuthenticator(CoreConfiguration cfg, ServiceAccountKey saKey) throws InvalidKeySpecException, IOException, ApiException { this.saKey = saKey; this.gson = new Gson(); this.httpClient = new OkHttpClient.Builder() @@ -86,7 +88,13 @@ public KeyFlowAuthenticator(CoreConfiguration cfg, ServiceAccountKey saKey) thro createAccessToken(); } - public synchronized String getAccessToken() throws IOException { + + /** + * Returns access token. If the token is expired it creates a new token. + * @throws IOException request for new access token failed + * @throws ApiException response for new access token with bad status code + */ + public synchronized String getAccessToken() throws IOException, ApiException { if (token == null || token.isExpired()) { createAccessTokenWithRefreshToken(); } @@ -94,12 +102,13 @@ public synchronized String getAccessToken() throws IOException { } /** - * Creates the inital accessToken and stores it in `this.token` + * Creates the initial accessToken and stores it in `this.token` * @throws InvalidKeySpecException can not parse private key * @throws IOException request for access token failed + * @throws ApiException response for new access token with bad status code * @throws JsonSyntaxException parsing of the created access token failed */ - private void createAccessToken() throws InvalidKeySpecException, IOException, JsonSyntaxException { + private void createAccessToken() throws InvalidKeySpecException, IOException, JsonSyntaxException, ApiException { String grant = "urn:ietf:params:oauth:grant-type:jwt-bearer"; String assertion; try { @@ -107,29 +116,22 @@ private void createAccessToken() throws InvalidKeySpecException, IOException, Js } catch (NoSuchAlgorithmException e) { throw new RuntimeException("could not find required algorithm for jwt signing. This should not happen and should be reported on https://github.com/stackitcloud/stackit-sdk-java/issues", e); } - try(Response response = requestToken(grant, assertion).execute()) { - parseTokenResponse(response); - } catch (IOException | ApiException e) { - throw new IOException("request for access token failed", e); - } catch (JsonSyntaxException e) { - throw new JsonSyntaxException("parsing access token failed", e); + Response response = requestToken(grant, assertion).execute(); + parseTokenResponse(response); + response.close(); } - } /** * Creates a new access token with the existing refresh token * @throws IOException request for new access token failed + * @throws ApiException response for new access token with bad status code * @throws JsonSyntaxException can not parse new access token */ - private synchronized void createAccessTokenWithRefreshToken() throws IOException, JsonSyntaxException { + private synchronized void createAccessTokenWithRefreshToken() throws IOException, JsonSyntaxException, ApiException { String refreshToken = token.refreshToken; - try (Response response = requestToken(REFRESH_TOKEN, refreshToken).execute()) { - parseTokenResponse(response); - } catch (IOException | ApiException e) { - throw new IOException("request for new access token failed", e); - } catch (JsonSyntaxException e) { - throw new JsonSyntaxException("parsing refreshed access token failed", e); - } + Response response = requestToken(REFRESH_TOKEN, refreshToken).execute(); + parseTokenResponse(response); + response.close(); } private synchronized void parseTokenResponse(Response response) throws ApiException, JsonSyntaxException { @@ -145,13 +147,9 @@ private synchronized void parseTokenResponse(Response response) throws ApiExcept throw new JsonSyntaxException("body from token creation is null"); } - try { - token = gson.fromJson(new InputStreamReader(response.body().byteStream(), StandardCharsets.UTF_8), KeyFlowTokenResponse.class); - token.expiresIn = JWT.decode(token.accessToken).getExpiresAt().toInstant().minusSeconds(tokenLeewayInSeconds).getEpochSecond(); - response.body().close(); - } catch (JsonSyntaxException e) { - throw new JsonSyntaxException("could not parse response of created token", e); - } + token = gson.fromJson(new InputStreamReader(response.body().byteStream(), StandardCharsets.UTF_8), KeyFlowTokenResponse.class); + token.expiresIn = JWT.decode(token.accessToken).getExpiresAt().toInstant().minusSeconds(tokenLeewayInSeconds).getEpochSecond(); + response.body().close(); } private Call requestToken(String grant, String assertionValue) throws IOException { @@ -171,11 +169,8 @@ private Call requestToken(String grant, String assertionValue) throws IOExceptio private String generateSelfSignedJWT() throws InvalidKeySpecException, NoSuchAlgorithmException { RSAPrivateKey prvKey; - try { - prvKey = saKey.getCredentials().getPrivateKeyParsed(); - } catch (InvalidKeySpecException e) { - throw new InvalidKeySpecException("could not parse private key", e); - } + + prvKey = saKey.getCredentials().getPrivateKeyParsed(); Algorithm algorithm = Algorithm.RSA512(prvKey); Map jwtHeader = new HashMap<>(); diff --git a/core/src/main/java/cloud/stackit/sdk/core/KeyFlowInterceptor.java b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowInterceptor.java index 77305b1..2c0b935 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/KeyFlowInterceptor.java +++ b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowInterceptor.java @@ -1,5 +1,6 @@ package cloud.stackit.sdk.core; +import cloud.stackit.sdk.core.exception.ApiException; import okhttp3.Interceptor; import okhttp3.Request; import okhttp3.Response; @@ -18,7 +19,14 @@ public KeyFlowInterceptor(KeyFlowAuthenticator authenticator) { @Override public Response intercept(Chain chain) throws IOException { Request originalRequest = chain.request(); - String accessToken = authenticator.getAccessToken(); + String accessToken; + try { + accessToken = authenticator.getAccessToken(); + } catch (ApiException e) { + // try-catch required, because ApiException can not be thrown in the implementation + // of Interceptor.intercept(Chain chain) + throw new RuntimeException(e); + } Request authenticatedRequest = originalRequest.newBuilder() .header("Authorization", "Bearer " + accessToken) diff --git a/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java b/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java index 308a1a3..269b428 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java +++ b/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java @@ -1,9 +1,12 @@ package cloud.stackit.sdk.core.auth; +import cloud.stackit.sdk.core.exception.ApiException; import cloud.stackit.sdk.core.KeyFlowAuthenticator; import cloud.stackit.sdk.core.config.CoreConfiguration; import cloud.stackit.sdk.core.config.EnvironmentVariables; import cloud.stackit.sdk.core.KeyFlowInterceptor; +import cloud.stackit.sdk.core.exception.CredentialsInFileNotFoundException; +import cloud.stackit.sdk.core.exception.PrivateKeyNotFoundException; import cloud.stackit.sdk.core.model.ServiceAccountKey; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; @@ -32,22 +35,24 @@ public class SetupAuth { /** * Set up the KeyFlow Authentication and can be integrated in an OkHttp client, by adding `SetupAuth().getAuthHandler()` as interceptor. * This relies on the configuration methods via ENVs or the credentials file in `$HOME/.stackit/credentials.json` - * @throws IOException when no file can be found + * @throws IOException when a file can be found * @throws CredentialNotFoundException when no configuration is set or can be found * @throws InvalidKeySpecException when the private key can not be parsed + * @throws ApiException when access token creation failed */ - public SetupAuth() throws IOException, InvalidKeySpecException, CredentialNotFoundException { + public SetupAuth() throws IOException, InvalidKeySpecException, CredentialNotFoundException, ApiException { this(new CoreConfiguration.Builder().build()); } /** * Set up the KeyFlow Authentication and can be integrated in an OkHttp client, by adding `SetupAuth().getAuthHandler()` as interceptor. * @param cfg Configuration which describes, which service account and token endpoint should be used - * @throws IOException when no file can be found - * @throws CredentialNotFoundException when no configuration is set or can be found + * @throws IOException when a file can be found + * @throws CredentialsInFileNotFoundException when no credentials are set or can be found * @throws InvalidKeySpecException when the private key can not be parsed + * @throws ApiException when access token creation failed */ - public SetupAuth(CoreConfiguration cfg) throws IOException, CredentialNotFoundException, InvalidKeySpecException { + public SetupAuth(CoreConfiguration cfg) throws IOException, CredentialsInFileNotFoundException, InvalidKeySpecException, ApiException { if (cfg == null) { cfg = new CoreConfiguration.Builder().build(); } @@ -90,10 +95,10 @@ public Interceptor getAuthHandler() { * * @param cfg * @return ServiceAccountKey - * @throws CredentialNotFoundException throws error when no service account key or private key can be found - * @throws IOException throws an error if a file can not be found + * @throws CredentialsInFileNotFoundException thrown when no service account key or private key can be found + * @throws IOException thrown when a file can not be found */ - private ServiceAccountKey setupKeyFlow(CoreConfiguration cfg) throws CredentialNotFoundException, IOException { + private ServiceAccountKey setupKeyFlow(CoreConfiguration cfg) throws CredentialsInFileNotFoundException, IOException { // Explicit config in code if (cfg.getServiceAccountKey() != null && !cfg.getServiceAccountKey().trim().isEmpty()) { ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(cfg.getServiceAccountKey()); @@ -135,13 +140,13 @@ private ServiceAccountKey setupKeyFlow(CoreConfiguration cfg) throws CredentialN } } - private void loadPrivateKey(CoreConfiguration cfg, ServiceAccountKey saKey) throws CredentialNotFoundException { + private void loadPrivateKey(CoreConfiguration cfg, ServiceAccountKey saKey) throws PrivateKeyNotFoundException { if (!saKey.getCredentials().isPrivateKeySet()) { try { String privateKey = getPrivateKey(cfg); saKey.getCredentials().setPrivateKey(privateKey); } catch (Exception e) { - throw new CredentialNotFoundException("could not find private key\n" + e.getMessage()); + throw new PrivateKeyNotFoundException("could not find private key", e); } } } @@ -175,10 +180,10 @@ private void loadPrivateKey(CoreConfiguration cfg, ServiceAccountKey saKey) thro * * @param cfg * @return found private key - * @throws CredentialNotFoundException throws if no private key could be found + * @throws CredentialsInFileNotFoundException throws if no private key could be found * @throws IOException throws if the provided path can not be found or the file within the pathKey can not be found */ - private String getPrivateKey(CoreConfiguration cfg) throws CredentialNotFoundException, IOException { + private String getPrivateKey(CoreConfiguration cfg) throws CredentialsInFileNotFoundException, IOException { // Explicit code config // Set private key if (cfg.getPrivateKey() != null && !cfg.getPrivateKey().trim().isEmpty()) { @@ -215,10 +220,10 @@ private String getPrivateKey(CoreConfiguration cfg) throws CredentialNotFoundExc * @param valueKey key which contains the secret as value * @param pathKey key which contains a path to a file * @return Either the value of `valueKey` or the content of the file in `pathKey` - * @throws CredentialNotFoundException throws if no value was found in the credentials file + * @throws CredentialsInFileNotFoundException throws if no value was found in the credentials file * @throws IOException throws if the provided path can not be found or the file within the pathKey can not be found */ - private String readValueFromCredentialsFile(String path, String valueKey, String pathKey) throws IOException, CredentialNotFoundException { + private String readValueFromCredentialsFile(String path, String valueKey, String pathKey) throws IOException, CredentialsInFileNotFoundException { // Read credentials file String fileContent = new String(Files.readAllBytes(Paths.get(path)), StandardCharsets.UTF_8); Type credentialsFileType = new TypeToken>(){}.getType(); @@ -241,6 +246,6 @@ private String readValueFromCredentialsFile(String path, String valueKey, String if (keyPath != null && !keyPath.trim().isEmpty()) { return new String(Files.readAllBytes(Paths.get(keyPath))); } - throw new CredentialNotFoundException("could not find " + valueKey + " or " + pathKey + " in " + path); + throw new CredentialsInFileNotFoundException("could not find " + valueKey + " or " + pathKey + " in " + path); } } diff --git a/core/src/main/java/cloud/stackit/sdk/core/ApiException.java b/core/src/main/java/cloud/stackit/sdk/core/exception/ApiException.java similarity index 99% rename from core/src/main/java/cloud/stackit/sdk/core/ApiException.java rename to core/src/main/java/cloud/stackit/sdk/core/exception/ApiException.java index d3cb409..88f934b 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/ApiException.java +++ b/core/src/main/java/cloud/stackit/sdk/core/exception/ApiException.java @@ -1,4 +1,4 @@ -package cloud.stackit.sdk.core; +package cloud.stackit.sdk.core.exception; import java.util.List; diff --git a/core/src/main/java/cloud/stackit/sdk/core/exception/CredentialsInFileNotFoundException.java b/core/src/main/java/cloud/stackit/sdk/core/exception/CredentialsInFileNotFoundException.java new file mode 100644 index 0000000..75d5e51 --- /dev/null +++ b/core/src/main/java/cloud/stackit/sdk/core/exception/CredentialsInFileNotFoundException.java @@ -0,0 +1,12 @@ +package cloud.stackit.sdk.core.exception; + +public class CredentialsInFileNotFoundException extends RuntimeException { + + public CredentialsInFileNotFoundException(String msg) { + super(msg); + } + + public CredentialsInFileNotFoundException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/core/src/main/java/cloud/stackit/sdk/core/exception/PrivateKeyNotFoundException.java b/core/src/main/java/cloud/stackit/sdk/core/exception/PrivateKeyNotFoundException.java new file mode 100644 index 0000000..711eafa --- /dev/null +++ b/core/src/main/java/cloud/stackit/sdk/core/exception/PrivateKeyNotFoundException.java @@ -0,0 +1,12 @@ +package cloud.stackit.sdk.core.exception; + +public class PrivateKeyNotFoundException extends RuntimeException { + + public PrivateKeyNotFoundException(String msg) { + super(msg); + } + + public PrivateKeyNotFoundException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java index 9ce17af..4e3d469 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java +++ b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java @@ -78,11 +78,10 @@ public RSAPublicKey getPublicKeyParsed() throws NoSuchAlgorithmException, Invali byte[] publicBytes = Base64.getDecoder().decode(trimmedKey); X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicBytes); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - RSAPublicKey pubKey = (RSAPublicKey) keyFactory.generatePublic(keySpec); - return pubKey; + return (RSAPublicKey) keyFactory.generatePublic(keySpec); } - public static ServiceAccountKey loadFromJson(String json) throws com.google.gson.JsonSyntaxException { + public static ServiceAccountKey loadFromJson(String json) throws JsonSyntaxException { ServiceAccountKey saKey = new Gson().fromJson(json, ServiceAccountKey.class); if (!saKey.isCredentialsSet()) { throw new JsonSyntaxException("required field `credentials` in service account key is missing."); diff --git a/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/ApiClient.java b/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/ApiClient.java index 66eff18..082b248 100644 --- a/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/ApiClient.java +++ b/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/ApiClient.java @@ -118,7 +118,7 @@ public ApiClient(OkHttpClient client) { authentications = Collections.unmodifiableMap(authentications); } - public ApiClient(CoreConfiguration config) throws IOException, InvalidKeySpecException, CredentialNotFoundException { + public ApiClient(CoreConfiguration config) throws IOException, InvalidKeySpecException, cloud.stackit.sdk.core.exception.ApiException { init(); if (config.getCustomEndpoint() != null && !config.getCustomEndpoint().trim().isEmpty()) { diff --git a/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/api/DefaultApi.java b/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/api/DefaultApi.java index 07938ec..c3769bd 100644 --- a/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/api/DefaultApi.java +++ b/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/api/DefaultApi.java @@ -59,7 +59,8 @@ public DefaultApi(ApiClient apiClient) { this.localVarApiClient = apiClient; } - public DefaultApi(CoreConfiguration config) throws IOException, InvalidKeySpecException, CredentialNotFoundException { + // TODO: remove in follow up story the service specific ApiException and use instead the ApiException of core + public DefaultApi(CoreConfiguration config) throws IOException, InvalidKeySpecException, cloud.stackit.sdk.core.exception.ApiException { if (config.getCustomEndpoint() != null && !config.getCustomEndpoint().trim().isEmpty()) { localCustomBaseUrl = config.getCustomEndpoint(); } diff --git a/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/main.java b/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/main.java index 9ca5939..8a78743 100644 --- a/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/main.java +++ b/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/main.java @@ -9,7 +9,7 @@ import java.security.spec.InvalidKeySpecException; public class main { - public static void main(String[] args) { + public static void main(String[] args) throws IOException, InvalidKeySpecException, ApiException, cloud.stackit.sdk.core.exception.ApiException { String SERVICE_ACCOUNT_KEY_PATH = "/path/to/your/sa/key.json"; String SERIVCE_ACCOUNT_MAIL = "name-1234@sa.stackit.cloud"; From 9517d9f05708d8b8380c7dfc092594f3da72b8ab Mon Sep 17 00:00:00 2001 From: Marcel Jacek Date: Fri, 1 Aug 2025 17:31:06 +0200 Subject: [PATCH 06/12] wip: add tests --- .../sdk/core/KeyFlowAuthenticator.java | 2 +- .../stackit/sdk/core/auth/SetupAuth.java | 4 +- .../core/model/ServiceAccountCredentials.java | 6 +- .../sdk/core/model/ServiceAccountKey.java | 17 -- .../core/config/CoreConfigurationTest.java | 200 ++++++++++++++++++ .../model/ServiceAccountCredentialsTest.java | 84 ++++++++ .../sdk/core/model/ServiceAccountKeyTest.java | 105 +++++++++ 7 files changed, 395 insertions(+), 23 deletions(-) create mode 100644 core/src/test/java/cloud/stackit/sdk/core/config/CoreConfigurationTest.java create mode 100644 core/src/test/java/cloud/stackit/sdk/core/model/ServiceAccountCredentialsTest.java create mode 100644 core/src/test/java/cloud/stackit/sdk/core/model/ServiceAccountKeyTest.java diff --git a/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java index 44abf93..c57727d 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java +++ b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java @@ -178,7 +178,7 @@ private String generateSelfSignedJWT() throws InvalidKeySpecException, NoSuchAlg return JWT.create() .withIssuer(saKey.getCredentials().getIss()) - .withSubject(saKey.getCredentials().getSub().toString()) + .withSubject(saKey.getCredentials().getSub()) .withJWTId(UUID.randomUUID().toString()) .withAudience(saKey.getCredentials().getAud()) .withIssuedAt(new Date()) diff --git a/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java b/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java index 269b428..4dc5d06 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java +++ b/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java @@ -36,11 +36,11 @@ public class SetupAuth { * Set up the KeyFlow Authentication and can be integrated in an OkHttp client, by adding `SetupAuth().getAuthHandler()` as interceptor. * This relies on the configuration methods via ENVs or the credentials file in `$HOME/.stackit/credentials.json` * @throws IOException when a file can be found - * @throws CredentialNotFoundException when no configuration is set or can be found + * @throws CredentialsInFileNotFoundException when no configuration is set or can be found * @throws InvalidKeySpecException when the private key can not be parsed * @throws ApiException when access token creation failed */ - public SetupAuth() throws IOException, InvalidKeySpecException, CredentialNotFoundException, ApiException { + public SetupAuth() throws IOException, InvalidKeySpecException, CredentialsInFileNotFoundException, ApiException { this(new CoreConfiguration.Builder().build()); } diff --git a/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountCredentials.java b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountCredentials.java index 8778d99..b9efc50 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountCredentials.java +++ b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountCredentials.java @@ -13,9 +13,9 @@ public class ServiceAccountCredentials { private final String iss; private final String kid; private String privateKey; - private final UUID sub; + private final String sub; - public ServiceAccountCredentials(String aud, String iss, String kid, String privateKey, UUID sub) { + public ServiceAccountCredentials(String aud, String iss, String kid, String privateKey, String sub) { this.aud = aud; this.iss = iss; this.kid = kid; @@ -47,7 +47,7 @@ public boolean isPrivateKeySet() { return privateKey != null && !privateKey.trim().isEmpty(); } - public UUID getSub() { + public String getSub() { return sub; } diff --git a/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java index 4e3d469..0ca46d1 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java +++ b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java @@ -3,12 +3,6 @@ import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; -import java.security.interfaces.RSAPublicKey; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.X509EncodedKeySpec; -import java.util.Base64; import java.util.Date; public class ServiceAccountKey { @@ -70,17 +64,6 @@ public ServiceAccountCredentials getCredentials() { return credentials; } - public RSAPublicKey getPublicKeyParsed() throws NoSuchAlgorithmException, InvalidKeySpecException { - String trimmedKey = publicKey.replaceFirst("-----BEGIN PUBLIC KEY-----", ""); - trimmedKey = trimmedKey.replaceFirst("-----END PUBLIC KEY-----", ""); - trimmedKey = trimmedKey.replaceAll("\n",""); - - byte[] publicBytes = Base64.getDecoder().decode(trimmedKey); - X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicBytes); - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - return (RSAPublicKey) keyFactory.generatePublic(keySpec); - } - public static ServiceAccountKey loadFromJson(String json) throws JsonSyntaxException { ServiceAccountKey saKey = new Gson().fromJson(json, ServiceAccountKey.class); if (!saKey.isCredentialsSet()) { diff --git a/core/src/test/java/cloud/stackit/sdk/core/config/CoreConfigurationTest.java b/core/src/test/java/cloud/stackit/sdk/core/config/CoreConfigurationTest.java new file mode 100644 index 0000000..70831f8 --- /dev/null +++ b/core/src/test/java/cloud/stackit/sdk/core/config/CoreConfigurationTest.java @@ -0,0 +1,200 @@ +package cloud.stackit.sdk.core.config; + +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class CoreConfigurationTest { + + @Test + void getDefaultHeader() { + HashMap map = new HashMap(); + map.put("key", "value"); + CoreConfiguration cfg = + new CoreConfiguration.Builder(). + defaultHeader(map). + build(); + Map cfgHeader = cfg.getDefaultHeader(); + + assertEquals(map, cfgHeader); + } + + @Test + void getServiceAccountKey() { + final String saKey = ""; + + CoreConfiguration cfg = new CoreConfiguration.Builder() + .serviceAccountKey(saKey) + .build(); + + String cfgSaKey = cfg.getServiceAccountKey(); + + assertEquals(saKey, cfgSaKey); + } + + @Test + void getServiceAccountKeyPath() { + final String saKeyPath = ""; + + CoreConfiguration cfg = new CoreConfiguration.Builder() + .serviceAccountKeyPath(saKeyPath) + .build(); + + String cfgSaKeyPath = cfg.getServiceAccountKeyPath(); + + assertEquals(saKeyPath, cfgSaKeyPath); + } + + @Test + void getPrivateKeyPath() { + final String privateKeyPath = ""; + + CoreConfiguration cfg = new CoreConfiguration.Builder() + .privateKeyPath(privateKeyPath) + .build(); + + String cfgPrivateKeyPath = cfg.getPrivateKeyPath(); + + assertEquals(privateKeyPath, cfgPrivateKeyPath); + } + + @Test + void getPrivateKey() { + final String privateKey = ""; + + CoreConfiguration cfg = new CoreConfiguration.Builder() + .privateKey(privateKey) + .build(); + + String cfgPrivateKey = cfg.getPrivateKey(); + + assertEquals(privateKey, cfgPrivateKey); + } + + @Test + void getCustomEndpoint() { + final String customEndpoint = ""; + + CoreConfiguration cfg = new CoreConfiguration.Builder() + .customEndpoint(customEndpoint) + .build(); + + String cfgCustomEndpoint = cfg.getCustomEndpoint(); + + assertEquals(customEndpoint, cfgCustomEndpoint); + } + + @Test + void getCredentialsFilePath() { + final String credFilePath = ""; + + CoreConfiguration cfg = new CoreConfiguration.Builder() + .credentialsFilePath(credFilePath) + .build(); + + String cfgCredentialsFilePath = cfg.getCredentialsFilePath(); + + assertEquals(credFilePath, cfgCredentialsFilePath); + } + + @Test + void getTokenCustomUrl() { + final String tokenCustomUrl = ""; + + CoreConfiguration cfg = new CoreConfiguration.Builder() + .tokenCustomUrl(tokenCustomUrl) + .build(); + + String cfgTokenUrl = cfg.getTokenCustomUrl(); + + assertEquals(tokenCustomUrl, cfgTokenUrl); + } + + @Test + void getTokenExpirationLeeway() { + final long tokenExpireLeeway = 100; + + CoreConfiguration cfg = new CoreConfiguration.Builder() + .tokenExpirationLeeway(tokenExpireLeeway) + .build(); + + Long cfgTokenExpirationLeeway = cfg.getTokenExpirationLeeway(); + + assertEquals(tokenExpireLeeway, cfgTokenExpirationLeeway); + } + + @Test + void getDefaultHeader_not_set() { + CoreConfiguration cfg = new CoreConfiguration.Builder().build(); + Map defaultHeader = cfg.getDefaultHeader(); + + assertNull(defaultHeader); + } + + @Test + void getServiceAccountKey_not_set() { + CoreConfiguration cfg = new CoreConfiguration.Builder().build(); + String serviceAccountKey = cfg.getServiceAccountKey(); + + assertNull(serviceAccountKey); + } + + @Test + void getServiceAccountKeyPath_not_set() { + CoreConfiguration cfg = new CoreConfiguration.Builder().build(); + String serviceAccountKeyPath = cfg.getServiceAccountKeyPath(); + + assertNull(serviceAccountKeyPath); + } + + @Test + void getPrivateKeyPath_not_set() { + CoreConfiguration cfg = new CoreConfiguration.Builder().build(); + String privateKeyPath = cfg.getPrivateKeyPath(); + + assertNull(privateKeyPath); + } + + @Test + void getPrivateKey_not_set() { + CoreConfiguration cfg = new CoreConfiguration.Builder().build(); + String privateKey = cfg.getPrivateKey(); + + assertNull(privateKey); + } + + @Test + void getCustomEndpoint_not_set() { + CoreConfiguration cfg = new CoreConfiguration.Builder().build(); + String customEndpoint = cfg.getCustomEndpoint(); + + assertNull(customEndpoint); + } + + @Test + void getCredentialsFilePath_not_set() { + CoreConfiguration cfg = new CoreConfiguration.Builder().build(); + String credentialsFilePath = cfg.getCredentialsFilePath(); + + assertNull(credentialsFilePath); + } + + @Test + void getTokenCustomUrl_not_set() { + CoreConfiguration cfg = new CoreConfiguration.Builder().build(); + String tokenCustomUrl = cfg.getTokenCustomUrl(); + + assertNull(tokenCustomUrl); + } + + @Test + void getTokenExpirationLeeway_not_set() { + CoreConfiguration cfg = new CoreConfiguration.Builder().build(); + Long tokenExpirationLeeway = cfg.getTokenExpirationLeeway(); + + assertNull(tokenExpirationLeeway); + } +} \ No newline at end of file diff --git a/core/src/test/java/cloud/stackit/sdk/core/model/ServiceAccountCredentialsTest.java b/core/src/test/java/cloud/stackit/sdk/core/model/ServiceAccountCredentialsTest.java new file mode 100644 index 0000000..2272cd7 --- /dev/null +++ b/core/src/test/java/cloud/stackit/sdk/core/model/ServiceAccountCredentialsTest.java @@ -0,0 +1,84 @@ +package cloud.stackit.sdk.core.model; + +import org.junit.jupiter.api.Test; + +import java.security.spec.InvalidKeySpecException; + +import static org.junit.jupiter.api.Assertions.*; + +class ServiceAccountCredentialsTest { + + @Test + void isPrivateKeySet_null_returnsFalse() { + ServiceAccountCredentials saCreds = new ServiceAccountCredentials(null, null, null, null, null); + + assertFalse( + saCreds.isPrivateKeySet() + ); + } + + @Test + void isPrivateKeySet_emptyString_returnsFalse() { + ServiceAccountCredentials saCreds = new ServiceAccountCredentials(null, null, null, "", null); + + assertFalse( + saCreds.isPrivateKeySet() + ); + } + + @Test + void isPrivateKeySet_emptyStringWhitespaces_returnsFalse() { + ServiceAccountCredentials saCreds = new ServiceAccountCredentials(null, null, null, " ", null); + + assertFalse( + saCreds.isPrivateKeySet() + ); + } + + @Test + void isPrivateKeySet_string_returnsFalse() { + ServiceAccountCredentials saCreds = new ServiceAccountCredentials(null, null, null, "my-private-key", null); + + assertTrue( + saCreds.isPrivateKeySet() + ); + } + + @Test + void getPrivateKeyParsed_notBase64Key_throwsException() { + ServiceAccountCredentials saCreds = new ServiceAccountCredentials(null, null, null, "my-private-key", null); + + assertThrows( + IllegalArgumentException.class, + saCreds::getPrivateKeyParsed + ); + } + + @Test + void getPrivateKeyParsed_invalidKey_throwsException() { + ServiceAccountCredentials saCreds = new ServiceAccountCredentials(null, null, null, "bXktcHJpdmF0ZS1rZXk=", null); + + assertThrows( + InvalidKeySpecException.class, + saCreds::getPrivateKeyParsed + ); + } + + @Test + void getPrivateKeyParsed_validKey_returnsRsaKey() { + final String privateKey = "-----BEGIN PRIVATE KEY-----\n" + + "MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqPfgaTEWEP3S9w0t\n" + + "gsicURfo+nLW09/0KfOPinhYZ4ouzU+3xC4pSlEp8Ut9FgL0AgqNslNaK34Kq+NZ\n" + + "jO9DAQIDAQABAkAgkuLEHLaqkWhLgNKagSajeobLS3rPT0Agm0f7k55FXVt743hw\n" + + "Ngkp98bMNrzy9AQ1mJGbQZGrpr4c8ZAx3aRNAiEAoxK/MgGeeLui385KJ7ZOYktj\n" + + "hLBNAB69fKwTZFsUNh0CIQEJQRpFCcydunv2bENcN/oBTRw39E8GNv2pIcNxZkcb\n" + + "NQIgbYSzn3Py6AasNj6nEtCfB+i1p3F35TK/87DlPSrmAgkCIQDJLhFoj1gbwRbH\n" + + "/bDRPrtlRUDDx44wHoEhSDRdy77eiQIgE6z/k6I+ChN1LLttwX0galITxmAYrOBh\n" + + "BVl433tgTTQ=\n" + + "-----END PRIVATE KEY-----"; + + ServiceAccountCredentials saCreds = new ServiceAccountCredentials(null, null, null, privateKey, null); + + assertDoesNotThrow(saCreds::getPrivateKeyParsed); + } +} \ No newline at end of file diff --git a/core/src/test/java/cloud/stackit/sdk/core/model/ServiceAccountKeyTest.java b/core/src/test/java/cloud/stackit/sdk/core/model/ServiceAccountKeyTest.java new file mode 100644 index 0000000..0ff4030 --- /dev/null +++ b/core/src/test/java/cloud/stackit/sdk/core/model/ServiceAccountKeyTest.java @@ -0,0 +1,105 @@ +package cloud.stackit.sdk.core.model; + +import com.google.gson.JsonSyntaxException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ServiceAccountKeyTest { + + @Test + void loadFromJson_validJson_returnSaKey() { + final String uuid = "6d778bbf-6c86-46e6-952a-0c1b5fd87be3"; + final String iss = "service-account-test@sa.stackit.cloud"; + final String aud = "https://aud.stackit.cloud"; + + final String privateKey = "-----BEGIN PRIVATE KEY-----\n" + + "MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqPfgaTEWEP3S9w0t\n" + + "gsicURfo+nLW09/0KfOPinhYZ4ouzU+3xC4pSlEp8Ut9FgL0AgqNslNaK34Kq+NZ\n" + + "jO9DAQIDAQABAkAgkuLEHLaqkWhLgNKagSajeobLS3rPT0Agm0f7k55FXVt743hw\n" + + "Ngkp98bMNrzy9AQ1mJGbQZGrpr4c8ZAx3aRNAiEAoxK/MgGeeLui385KJ7ZOYktj\n" + + "hLBNAB69fKwTZFsUNh0CIQEJQRpFCcydunv2bENcN/oBTRw39E8GNv2pIcNxZkcb\n" + + "NQIgbYSzn3Py6AasNj6nEtCfB+i1p3F35TK/87DlPSrmAgkCIQDJLhFoj1gbwRbH\n" + + "/bDRPrtlRUDDx44wHoEhSDRdy77eiQIgE6z/k6I+ChN1LLttwX0galITxmAYrOBh\n" + + "BVl433tgTTQ=\n" + + "-----END PRIVATE KEY-----"; + + final String jsonContent = "{\n" + + " \"id\": \"" + uuid + "\",\n" + + " \"publicKey\": \"-----BEGIN PUBLIC KEY-----\\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf\\n9Cnzj4p4WGeKLs1Pt8QuKUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQ==\\n-----END PUBLIC KEY-----\",\n" + + " \"createdAt\": \"2025-01-01T01:00:00.000+00:00\",\n" + + " \"keyType\": \"USER_MANAGED\",\n" + + " \"keyOrigin\": \"GENERATED\",\n" + + " \"keyAlgorithm\": \"RSA_2048\",\n" + + " \"active\": true,\n" + + " \"credentials\": {\n" + + " \"kid\": \"" + uuid + "\",\n" + + " \"iss\": \"" + iss + "\",\n" + + " \"sub\": \"" + uuid + "\",\n" + + " \"aud\": \"" + aud + "\",\n" + + " \"privateKey\": \"" + privateKey + "\"\n" + + " }\n" + + "}\n"; + + assertDoesNotThrow(() -> ServiceAccountKey.loadFromJson(jsonContent)); + ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(jsonContent); + + assertEquals(uuid, saKey.getId()); + assertEquals(uuid, saKey.getCredentials().getKid()); + assertEquals(iss, saKey.getCredentials().getIss()); + assertEquals(uuid, saKey.getCredentials().getSub()); + assertEquals(aud, saKey.getCredentials().getAud()); + assertEquals(privateKey, saKey.getCredentials().getPrivateKey()); + } + + @Test + void loadFromJson_validJsonWithoutCredentials_throwsException() { + final String jsonContent = "{\n" + + " \"id\": \"6d778bbf-6c86-46e6-952a-0c1b5fd87be3\",\n" + + " \"publicKey\": \"-----BEGIN PUBLIC KEY-----\\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf\\n9Cnzj4p4WGeKLs1Pt8QuKUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQ==\\n-----END PUBLIC KEY-----\",\n" + + " \"createdAt\": \"2025-01-01T01:00:00.000+00:00\",\n" + + " \"keyType\": \"USER_MANAGED\",\n" + + " \"keyOrigin\": \"GENERATED\",\n" + + " \"keyAlgorithm\": \"RSA_2048\",\n" + + " \"active\": true\n" + + "}\n"; + + assertThrows( + JsonSyntaxException.class, + () -> ServiceAccountKey.loadFromJson(jsonContent) + ); + } + + @Test + void loadFromJson_validJsonWithoutPrivateKey_returnSaKey() { + final String uuid = "6d778bbf-6c86-46e6-952a-0c1b5fd87be3"; + final String iss = "service-account-test@sa.stackit.cloud"; + final String aud = "https://aud.stackit.cloud"; + + final String jsonContent = "{\n" + + " \"id\": \"" + uuid + "\",\n" + + " \"publicKey\": \"-----BEGIN PUBLIC KEY-----\\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf\\n9Cnzj4p4WGeKLs1Pt8QuKUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQ==\\n-----END PUBLIC KEY-----\",\n" + + " \"createdAt\": \"2025-01-01T01:00:00.000+00:00\",\n" + + " \"keyType\": \"USER_MANAGED\",\n" + + " \"keyOrigin\": \"GENERATED\",\n" + + " \"keyAlgorithm\": \"RSA_2048\",\n" + + " \"active\": true,\n" + + " \"credentials\": {\n" + + " \"kid\": \"" + uuid + "\",\n" + + " \"iss\": \"" + iss + "\",\n" + + " \"sub\": \"" + uuid + "\",\n" + + " \"aud\": \"" + aud + "\"\n" + + " }\n" + + "}\n"; + + assertDoesNotThrow(() -> ServiceAccountKey.loadFromJson(jsonContent)); + ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(jsonContent); + + assertEquals(uuid, saKey.getId()); + assertEquals(uuid, saKey.getCredentials().getKid()); + assertEquals(iss, saKey.getCredentials().getIss()); + assertEquals(uuid, saKey.getCredentials().getSub()); + assertEquals(aud, saKey.getCredentials().getAud()); + } + +} \ No newline at end of file From b3680029ec47285d1b89d59d7e5db6f4ed74b579 Mon Sep 17 00:00:00 2001 From: Marcel Jacek Date: Fri, 1 Aug 2025 17:56:38 +0200 Subject: [PATCH 07/12] moved authentication example from resourcemanager to example module - run formatter - configured build.gradle for core module --- core/build.gradle | 5 + .../cloud/stackit/sdk/core/CoreDummy.java | 3 - .../sdk/core/KeyFlowAuthenticator.java | 351 ++++++------ .../stackit/sdk/core/KeyFlowInterceptor.java | 46 +- .../stackit/sdk/core/auth/SetupAuth.java | 514 ++++++++++-------- .../sdk/core/config/CoreConfiguration.java | 236 ++++---- .../sdk/core/config/EnvironmentVariables.java | 64 ++- .../sdk/core/exception/ApiException.java | 311 ++++++----- .../CredentialsInFileNotFoundException.java | 12 +- .../PrivateKeyNotFoundException.java | 12 +- .../core/model/ServiceAccountCredentials.java | 87 +-- .../sdk/core/model/ServiceAccountKey.java | 125 +++-- .../core/config/CoreConfigurationTest.java | 268 +++++---- .../model/ServiceAccountCredentialsTest.java | 147 +++-- .../sdk/core/model/ServiceAccountKeyTest.java | 198 ++++--- examples/authentication/build.gradle | 3 + .../examples/AuthenticationExample.java | 29 + services/resourcemanager/build.gradle | 1 + .../sdk/resourcemanager/ApiClient.java | 44 +- .../sdk/resourcemanager/api/DefaultApi.java | 31 +- .../stackit/sdk/resourcemanager/main.java | 26 - 21 files changed, 1327 insertions(+), 1186 deletions(-) delete mode 100644 core/src/main/java/cloud/stackit/sdk/core/CoreDummy.java create mode 100644 examples/authentication/build.gradle create mode 100644 examples/authentication/src/main/java/cloud/stackit/sdk/authentication/examples/AuthenticationExample.java delete mode 100644 services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/main.java diff --git a/core/build.gradle b/core/build.gradle index 8b13789..9e58e22 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -1 +1,6 @@ +dependencies { + implementation 'com.auth0:java-jwt:4.5.0' + implementation 'com.squareup.okhttp3:okhttp:4.12.0' + implementation 'com.google.code.gson:gson:2.9.1' +} diff --git a/core/src/main/java/cloud/stackit/sdk/core/CoreDummy.java b/core/src/main/java/cloud/stackit/sdk/core/CoreDummy.java deleted file mode 100644 index 1a3b3a4..0000000 --- a/core/src/main/java/cloud/stackit/sdk/core/CoreDummy.java +++ /dev/null @@ -1,3 +0,0 @@ -package cloud.stackit.sdk.core; - -public class CoreDummy {} diff --git a/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java index c57727d..97d51ae 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java +++ b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java @@ -8,8 +8,6 @@ import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; import com.google.gson.annotations.SerializedName; -import okhttp3.*; - import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; @@ -22,168 +20,193 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; +import okhttp3.*; -/** - * KeyFlowAuthenticator handles the Key Flow Authentication based on the Service Account Key. - */ +/** KeyFlowAuthenticator handles the Key Flow Authentication based on the Service Account Key. */ public class KeyFlowAuthenticator { - private final String REFRESH_TOKEN = "refresh_token"; - private final String ASSERTION = "assertion"; - private final String DEFAULT_TOKEN_ENDPOINT = "https://service-account.api.stackit.cloud/token"; - private final long DEFAULT_TOKEN_LEEWAY = 60; - - private final OkHttpClient httpClient; - private final ServiceAccountKey saKey; - private KeyFlowTokenResponse token; - private final Gson gson; - private final String tokenUrl; - private long tokenLeewayInSeconds = DEFAULT_TOKEN_LEEWAY; - - private static class KeyFlowTokenResponse { - @SerializedName("access_token") - private String accessToken; - @SerializedName("refresh_token") - private String refreshToken; - @SerializedName("expires_in") - private long expiresIn; - @SerializedName("scope") - private String scope; - @SerializedName("token_type") - private String tokenType; - - public boolean isExpired() { - return expiresIn < new Date().toInstant().getEpochSecond(); - } - - public String getAccessToken() { - return accessToken; - } - } - - /** - * Creates the initial service account and refreshes expired access token. - * @param cfg Configuration to set a custom token endpoint and the token expiration leeway. - * @param saKey Service Account Key, which should be used for the authentication - * @throws InvalidKeySpecException thrown when the private key in the service account can not be parsed - * @throws IOException thrown on unexpected responses from the key flow - * @throws ApiException thrown on unexpected responses from the key flow - */ - public KeyFlowAuthenticator(CoreConfiguration cfg, ServiceAccountKey saKey) throws InvalidKeySpecException, IOException, ApiException { - this.saKey = saKey; - this.gson = new Gson(); - this.httpClient = new OkHttpClient.Builder() - .connectTimeout(10, TimeUnit.SECONDS) - .writeTimeout(10, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .build(); - if (cfg.getTokenCustomUrl() != null && !cfg.getTokenCustomUrl().trim().isEmpty()) { - this.tokenUrl = cfg.getTokenCustomUrl(); - } else { - this.tokenUrl = DEFAULT_TOKEN_ENDPOINT; - } - if (cfg.getTokenExpirationLeeway() != null && cfg.getTokenExpirationLeeway() > 0) { - this.tokenLeewayInSeconds = cfg.getTokenExpirationLeeway(); - } - - createAccessToken(); - } - - - /** - * Returns access token. If the token is expired it creates a new token. - * @throws IOException request for new access token failed - * @throws ApiException response for new access token with bad status code - */ - public synchronized String getAccessToken() throws IOException, ApiException { - if (token == null || token.isExpired()) { - createAccessTokenWithRefreshToken(); - } - return token.getAccessToken(); - } - - /** - * Creates the initial accessToken and stores it in `this.token` - * @throws InvalidKeySpecException can not parse private key - * @throws IOException request for access token failed - * @throws ApiException response for new access token with bad status code - * @throws JsonSyntaxException parsing of the created access token failed - */ - private void createAccessToken() throws InvalidKeySpecException, IOException, JsonSyntaxException, ApiException { - String grant = "urn:ietf:params:oauth:grant-type:jwt-bearer"; - String assertion; - try { - assertion = generateSelfSignedJWT(); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("could not find required algorithm for jwt signing. This should not happen and should be reported on https://github.com/stackitcloud/stackit-sdk-java/issues", e); - } - Response response = requestToken(grant, assertion).execute(); - parseTokenResponse(response); - response.close(); - } - - /** - * Creates a new access token with the existing refresh token - * @throws IOException request for new access token failed - * @throws ApiException response for new access token with bad status code - * @throws JsonSyntaxException can not parse new access token - */ - private synchronized void createAccessTokenWithRefreshToken() throws IOException, JsonSyntaxException, ApiException { - String refreshToken = token.refreshToken; - Response response = requestToken(REFRESH_TOKEN, refreshToken).execute(); - parseTokenResponse(response); - response.close(); - } - - private synchronized void parseTokenResponse(Response response) throws ApiException, JsonSyntaxException { - if (response.code() != HttpURLConnection.HTTP_OK) { - String body = null; - if (response.body() != null) { - body = response.body().toString(); - response.body().close(); - } - throw new ApiException(response.message(), response.code(), response.headers().toMultimap(), body); - } - if (response.body() == null) { - throw new JsonSyntaxException("body from token creation is null"); - } - - token = gson.fromJson(new InputStreamReader(response.body().byteStream(), StandardCharsets.UTF_8), KeyFlowTokenResponse.class); - token.expiresIn = JWT.decode(token.accessToken).getExpiresAt().toInstant().minusSeconds(tokenLeewayInSeconds).getEpochSecond(); - response.body().close(); - } - - private Call requestToken(String grant, String assertionValue) throws IOException { - FormBody.Builder bodyBuilder = new FormBody.Builder(); - bodyBuilder.addEncoded("grant_type", grant); - String assertionKey = grant.equals(REFRESH_TOKEN) ? REFRESH_TOKEN : ASSERTION; - bodyBuilder.addEncoded(assertionKey, assertionValue); - FormBody body = bodyBuilder.build(); - - Request request = new Request.Builder() - .url(tokenUrl) - .post(body) - .addHeader("Content-Type", "application/x-www-form-urlencoded") - .build(); - return httpClient.newCall(request); - } - - private String generateSelfSignedJWT() throws InvalidKeySpecException, NoSuchAlgorithmException { - RSAPrivateKey prvKey; - - prvKey = saKey.getCredentials().getPrivateKeyParsed(); - Algorithm algorithm = Algorithm.RSA512(prvKey); - - Map jwtHeader = new HashMap<>(); - jwtHeader.put("kid", saKey.getCredentials().getKid()); - - return JWT.create() - .withIssuer(saKey.getCredentials().getIss()) - .withSubject(saKey.getCredentials().getSub()) - .withJWTId(UUID.randomUUID().toString()) - .withAudience(saKey.getCredentials().getAud()) - .withIssuedAt(new Date()) - .withExpiresAt(new Date().toInstant().plusSeconds(10 * 60)) - .withHeader(jwtHeader) - .sign(algorithm); - } + private final String REFRESH_TOKEN = "refresh_token"; + private final String ASSERTION = "assertion"; + private final String DEFAULT_TOKEN_ENDPOINT = "https://service-account.api.stackit.cloud/token"; + private final long DEFAULT_TOKEN_LEEWAY = 60; + + private final OkHttpClient httpClient; + private final ServiceAccountKey saKey; + private KeyFlowTokenResponse token; + private final Gson gson; + private final String tokenUrl; + private long tokenLeewayInSeconds = DEFAULT_TOKEN_LEEWAY; + + private static class KeyFlowTokenResponse { + @SerializedName("access_token") + private String accessToken; + + @SerializedName("refresh_token") + private String refreshToken; + + @SerializedName("expires_in") + private long expiresIn; + + @SerializedName("scope") + private String scope; + + @SerializedName("token_type") + private String tokenType; + + public boolean isExpired() { + return expiresIn < new Date().toInstant().getEpochSecond(); + } + + public String getAccessToken() { + return accessToken; + } + } + + /** + * Creates the initial service account and refreshes expired access token. + * + * @param cfg Configuration to set a custom token endpoint and the token expiration leeway. + * @param saKey Service Account Key, which should be used for the authentication + * @throws InvalidKeySpecException thrown when the private key in the service account can not be + * parsed + * @throws IOException thrown on unexpected responses from the key flow + * @throws ApiException thrown on unexpected responses from the key flow + */ + public KeyFlowAuthenticator(CoreConfiguration cfg, ServiceAccountKey saKey) + throws InvalidKeySpecException, IOException, ApiException { + this.saKey = saKey; + this.gson = new Gson(); + this.httpClient = + new OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build(); + if (cfg.getTokenCustomUrl() != null && !cfg.getTokenCustomUrl().trim().isEmpty()) { + this.tokenUrl = cfg.getTokenCustomUrl(); + } else { + this.tokenUrl = DEFAULT_TOKEN_ENDPOINT; + } + if (cfg.getTokenExpirationLeeway() != null && cfg.getTokenExpirationLeeway() > 0) { + this.tokenLeewayInSeconds = cfg.getTokenExpirationLeeway(); + } + + createAccessToken(); + } + + /** + * Returns access token. If the token is expired it creates a new token. + * + * @throws IOException request for new access token failed + * @throws ApiException response for new access token with bad status code + */ + public synchronized String getAccessToken() throws IOException, ApiException { + if (token == null || token.isExpired()) { + createAccessTokenWithRefreshToken(); + } + return token.getAccessToken(); + } + + /** + * Creates the initial accessToken and stores it in `this.token` + * + * @throws InvalidKeySpecException can not parse private key + * @throws IOException request for access token failed + * @throws ApiException response for new access token with bad status code + * @throws JsonSyntaxException parsing of the created access token failed + */ + private void createAccessToken() + throws InvalidKeySpecException, IOException, JsonSyntaxException, ApiException { + String grant = "urn:ietf:params:oauth:grant-type:jwt-bearer"; + String assertion; + try { + assertion = generateSelfSignedJWT(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException( + "could not find required algorithm for jwt signing. This should not happen and should be reported on https://github.com/stackitcloud/stackit-sdk-java/issues", + e); + } + Response response = requestToken(grant, assertion).execute(); + parseTokenResponse(response); + response.close(); + } + + /** + * Creates a new access token with the existing refresh token + * + * @throws IOException request for new access token failed + * @throws ApiException response for new access token with bad status code + * @throws JsonSyntaxException can not parse new access token + */ + private synchronized void createAccessTokenWithRefreshToken() + throws IOException, JsonSyntaxException, ApiException { + String refreshToken = token.refreshToken; + Response response = requestToken(REFRESH_TOKEN, refreshToken).execute(); + parseTokenResponse(response); + response.close(); + } + + private synchronized void parseTokenResponse(Response response) + throws ApiException, JsonSyntaxException { + if (response.code() != HttpURLConnection.HTTP_OK) { + String body = null; + if (response.body() != null) { + body = response.body().toString(); + response.body().close(); + } + throw new ApiException( + response.message(), response.code(), response.headers().toMultimap(), body); + } + if (response.body() == null) { + throw new JsonSyntaxException("body from token creation is null"); + } + + token = + gson.fromJson( + new InputStreamReader(response.body().byteStream(), StandardCharsets.UTF_8), + KeyFlowTokenResponse.class); + token.expiresIn = + JWT.decode(token.accessToken) + .getExpiresAt() + .toInstant() + .minusSeconds(tokenLeewayInSeconds) + .getEpochSecond(); + response.body().close(); + } + + private Call requestToken(String grant, String assertionValue) throws IOException { + FormBody.Builder bodyBuilder = new FormBody.Builder(); + bodyBuilder.addEncoded("grant_type", grant); + String assertionKey = grant.equals(REFRESH_TOKEN) ? REFRESH_TOKEN : ASSERTION; + bodyBuilder.addEncoded(assertionKey, assertionValue); + FormBody body = bodyBuilder.build(); + + Request request = + new Request.Builder() + .url(tokenUrl) + .post(body) + .addHeader("Content-Type", "application/x-www-form-urlencoded") + .build(); + return httpClient.newCall(request); + } + + private String generateSelfSignedJWT() + throws InvalidKeySpecException, NoSuchAlgorithmException { + RSAPrivateKey prvKey; + + prvKey = saKey.getCredentials().getPrivateKeyParsed(); + Algorithm algorithm = Algorithm.RSA512(prvKey); + + Map jwtHeader = new HashMap<>(); + jwtHeader.put("kid", saKey.getCredentials().getKid()); + + return JWT.create() + .withIssuer(saKey.getCredentials().getIss()) + .withSubject(saKey.getCredentials().getSub()) + .withJWTId(UUID.randomUUID().toString()) + .withAudience(saKey.getCredentials().getAud()) + .withIssuedAt(new Date()) + .withExpiresAt(new Date().toInstant().plusSeconds(10 * 60)) + .withHeader(jwtHeader) + .sign(algorithm); + } } diff --git a/core/src/main/java/cloud/stackit/sdk/core/KeyFlowInterceptor.java b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowInterceptor.java index 2c0b935..ae67699 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/KeyFlowInterceptor.java +++ b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowInterceptor.java @@ -1,36 +1,36 @@ package cloud.stackit.sdk.core; import cloud.stackit.sdk.core.exception.ApiException; +import java.io.IOException; import okhttp3.Interceptor; import okhttp3.Request; import okhttp3.Response; import org.jetbrains.annotations.NotNull; -import java.io.IOException; - public class KeyFlowInterceptor implements Interceptor { - private final KeyFlowAuthenticator authenticator; + private final KeyFlowAuthenticator authenticator; - public KeyFlowInterceptor(KeyFlowAuthenticator authenticator) { - this.authenticator = authenticator; - } + public KeyFlowInterceptor(KeyFlowAuthenticator authenticator) { + this.authenticator = authenticator; + } - @NotNull - @Override - public Response intercept(Chain chain) throws IOException { - Request originalRequest = chain.request(); - String accessToken; - try { - accessToken = authenticator.getAccessToken(); - } catch (ApiException e) { - // try-catch required, because ApiException can not be thrown in the implementation - // of Interceptor.intercept(Chain chain) - throw new RuntimeException(e); - } + @NotNull @Override + public Response intercept(Chain chain) throws IOException { + Request originalRequest = chain.request(); + String accessToken; + try { + accessToken = authenticator.getAccessToken(); + } catch (ApiException e) { + // try-catch required, because ApiException can not be thrown in the implementation + // of Interceptor.intercept(Chain chain) + throw new RuntimeException(e); + } - Request authenticatedRequest = originalRequest.newBuilder() - .header("Authorization", "Bearer " + accessToken) - .build(); - return chain.proceed(authenticatedRequest); - } + Request authenticatedRequest = + originalRequest + .newBuilder() + .header("Authorization", "Bearer " + accessToken) + .build(); + return chain.proceed(authenticatedRequest); + } } diff --git a/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java b/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java index 4dc5d06..645c8bd 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java +++ b/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java @@ -1,19 +1,15 @@ package cloud.stackit.sdk.core.auth; -import cloud.stackit.sdk.core.exception.ApiException; import cloud.stackit.sdk.core.KeyFlowAuthenticator; +import cloud.stackit.sdk.core.KeyFlowInterceptor; import cloud.stackit.sdk.core.config.CoreConfiguration; import cloud.stackit.sdk.core.config.EnvironmentVariables; -import cloud.stackit.sdk.core.KeyFlowInterceptor; +import cloud.stackit.sdk.core.exception.ApiException; import cloud.stackit.sdk.core.exception.CredentialsInFileNotFoundException; import cloud.stackit.sdk.core.exception.PrivateKeyNotFoundException; import cloud.stackit.sdk.core.model.ServiceAccountKey; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; -import okhttp3.Interceptor; - -import javax.security.auth.login.CredentialNotFoundException; -import javax.swing.filechooser.FileSystemView; import java.io.File; import java.io.IOException; import java.lang.reflect.Type; @@ -22,230 +18,288 @@ import java.nio.file.Paths; import java.security.spec.InvalidKeySpecException; import java.util.Map; +import javax.swing.filechooser.FileSystemView; +import okhttp3.Interceptor; public class SetupAuth { - private final Interceptor authHandler; - private final String defaultCredentialsFilePath = - FileSystemView.getFileSystemView().getHomeDirectory() - + File.separator - + ".stackit" - + File.separator - + "credentials.json"; - - /** - * Set up the KeyFlow Authentication and can be integrated in an OkHttp client, by adding `SetupAuth().getAuthHandler()` as interceptor. - * This relies on the configuration methods via ENVs or the credentials file in `$HOME/.stackit/credentials.json` - * @throws IOException when a file can be found - * @throws CredentialsInFileNotFoundException when no configuration is set or can be found - * @throws InvalidKeySpecException when the private key can not be parsed - * @throws ApiException when access token creation failed - */ - public SetupAuth() throws IOException, InvalidKeySpecException, CredentialsInFileNotFoundException, ApiException { - this(new CoreConfiguration.Builder().build()); - } - - /** - * Set up the KeyFlow Authentication and can be integrated in an OkHttp client, by adding `SetupAuth().getAuthHandler()` as interceptor. - * @param cfg Configuration which describes, which service account and token endpoint should be used - * @throws IOException when a file can be found - * @throws CredentialsInFileNotFoundException when no credentials are set or can be found - * @throws InvalidKeySpecException when the private key can not be parsed - * @throws ApiException when access token creation failed - */ - public SetupAuth(CoreConfiguration cfg) throws IOException, CredentialsInFileNotFoundException, InvalidKeySpecException, ApiException { - if (cfg == null) { - cfg = new CoreConfiguration.Builder().build(); - } - - ServiceAccountKey saKey = setupKeyFlow(cfg); - authHandler = new KeyFlowInterceptor(new KeyFlowAuthenticator(cfg, saKey)); - } - - public Interceptor getAuthHandler() { - return authHandler; - } - - /** - * setupKeyFlow return first found ServiceAccountKey - * Reads the configured options in the following order - *
    - *
  1. - * Explicit configuration in `Configuration` - *
  2. - *
      - *
    • serviceAccountKey
    • - *
    • serviceAccountKeyPath
    • - *
    • credentialsFilePath -> STACKIT_SERVICE_ACCOUNT_KEY / STACKIT_SERVICE_ACCOUNT_KEY_PATH
    • - *
    - *
  3. - * Environment variables - *
  4. - *
      - *
    • STACKIT_SERVICE_ACCOUNT_KEY
    • - *
    • STACKIT_SERVICE_ACCOUNT_KEY_PATH
    • - *
    • STACKIT_CREDENTIALS_PATH -> STACKIT_SERVICE_ACCOUNT_KEY / STACKIT_SERVICE_ACCOUNT_KEY_PATH
    • - *
    - *
  5. - * Credentials file - *
  6. - *
      - *
    • STACKIT_SERVICE_ACCOUNT_KEY
    • - *
    • STACKIT_SERVICE_ACCOUNT_KEY_PATH
    • - *
    - *
- * @param cfg - * @return ServiceAccountKey - * @throws CredentialsInFileNotFoundException thrown when no service account key or private key can be found - * @throws IOException thrown when a file can not be found - */ - private ServiceAccountKey setupKeyFlow(CoreConfiguration cfg) throws CredentialsInFileNotFoundException, IOException { - // Explicit config in code - if (cfg.getServiceAccountKey() != null && !cfg.getServiceAccountKey().trim().isEmpty()) { - ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(cfg.getServiceAccountKey()); - loadPrivateKey(cfg, saKey); - return saKey; - } - - if (cfg.getServiceAccountKeyPath() != null && !cfg.getServiceAccountKeyPath().trim().isEmpty()) { - String fileContent = new String(Files.readAllBytes(Paths.get(cfg.getServiceAccountKeyPath())), StandardCharsets.UTF_8); - ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(fileContent); - loadPrivateKey(cfg, saKey); - return saKey; - } - - // Env config - if (EnvironmentVariables.STACKIT_SERVICE_ACCOUNT_KEY != null && !EnvironmentVariables.STACKIT_SERVICE_ACCOUNT_KEY.trim().isEmpty()) { - ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(EnvironmentVariables.STACKIT_SERVICE_ACCOUNT_KEY.trim()); - loadPrivateKey(cfg, saKey); - return saKey; - } - - if (EnvironmentVariables.STACKIT_SERVICE_ACCOUNT_KEY_PATH != null && !EnvironmentVariables.STACKIT_SERVICE_ACCOUNT_KEY_PATH.trim().isEmpty()) { - String fileContent = new String(Files.readAllBytes(Paths.get(cfg.getServiceAccountKeyPath())), StandardCharsets.UTF_8); - ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(fileContent); - loadPrivateKey(cfg, saKey); - return saKey; - } - - if (EnvironmentVariables.STACKIT_CREDENTIALS_PATH != null && !EnvironmentVariables.STACKIT_CREDENTIALS_PATH.trim().isEmpty()) { - String saKeyJson = readValueFromCredentialsFile(EnvironmentVariables.STACKIT_CREDENTIALS_PATH, EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY, EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY_PATH); - ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(saKeyJson); - loadPrivateKey(cfg, saKey); - return saKey; - } else { - String saKeyJson = readValueFromCredentialsFile(defaultCredentialsFilePath, EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY, EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY_PATH); - ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(saKeyJson); - loadPrivateKey(cfg, saKey); - return saKey; - } - } - - private void loadPrivateKey(CoreConfiguration cfg, ServiceAccountKey saKey) throws PrivateKeyNotFoundException { - if (!saKey.getCredentials().isPrivateKeySet()) { - try { - String privateKey = getPrivateKey(cfg); - saKey.getCredentials().setPrivateKey(privateKey); - } catch (Exception e) { - throw new PrivateKeyNotFoundException("could not find private key", e); - } - } - } - - /** - * Reads the private key in the following order - *
    - *
  1. - * Explicit configuration in `Configuration` - *
  2. - *
      - *
    • privateKey
    • - *
    • privateKeyPath
    • - *
    • credentialsFilePath -> STACKIT_PRIVATE_KEY / STACKIT_PRIVATE_KEY_PATH
    • - *
    - *
  3. - * Environment variables - *
  4. - *
      - *
    • STACKIT_PRIVATE_KEY
    • - *
    • STACKIT_PRIVATE_KEY_PATH
    • - *
    • STACKIT_CREDENTIALS_PATH -> STACKIT_PRIVATE_KEY / STACKIT_PRIVATE_KEY_PATH
    • - *
    - *
  5. - * Credentials file - *
  6. - *
      - *
    • STACKIT_PRIVATE_KEY
    • - *
    • STACKIT_PRIVATE_KEY_PATH
    • - *
    - *
- * @param cfg - * @return found private key - * @throws CredentialsInFileNotFoundException throws if no private key could be found - * @throws IOException throws if the provided path can not be found or the file within the pathKey can not be found - */ - private String getPrivateKey(CoreConfiguration cfg) throws CredentialsInFileNotFoundException, IOException { - // Explicit code config - // Set private key - if (cfg.getPrivateKey() != null && !cfg.getPrivateKey().trim().isEmpty()) { - return cfg.getPrivateKey(); - } - // Set private key path - if (cfg.getPrivateKeyPath() != null && !cfg.getPrivateKeyPath().trim().isEmpty()) { - String privateKeyPath = cfg.getPrivateKeyPath(); - return new String(Files.readAllBytes(Paths.get(privateKeyPath)), StandardCharsets.UTF_8); - } - // Set credentials file - if (cfg.getCredentialsFilePath() != null && !cfg.getCredentialsFilePath().trim().isEmpty()) { - return readValueFromCredentialsFile(cfg.getCredentialsFilePath(), EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY, EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY_PATH); - } - - // ENVs config - if (EnvironmentVariables.STACKIT_PRIVATE_KEY != null && !EnvironmentVariables.STACKIT_PRIVATE_KEY.trim().isEmpty()) { - return EnvironmentVariables.STACKIT_PRIVATE_KEY.trim(); - } - if (EnvironmentVariables.STACKIT_PRIVATE_KEY_PATH != null && !EnvironmentVariables.STACKIT_PRIVATE_KEY_PATH.trim().isEmpty()) { - return new String(Files.readAllBytes(Paths.get(EnvironmentVariables.STACKIT_PRIVATE_KEY_PATH)), StandardCharsets.UTF_8); - } - if (EnvironmentVariables.STACKIT_CREDENTIALS_PATH != null && !EnvironmentVariables.STACKIT_CREDENTIALS_PATH.trim().isEmpty()) { - return readValueFromCredentialsFile(EnvironmentVariables.STACKIT_CREDENTIALS_PATH, EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY, EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY_PATH); - } - - // Read from credentials file in defaultCredentialsFilePath - return readValueFromCredentialsFile(defaultCredentialsFilePath, EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY, EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY_PATH); - } - - /** - * Reads of a json credentials file from `path`, the values of `valueKey` or `pathKey`. - * @param path Path of the credentials file which should be read - * @param valueKey key which contains the secret as value - * @param pathKey key which contains a path to a file - * @return Either the value of `valueKey` or the content of the file in `pathKey` - * @throws CredentialsInFileNotFoundException throws if no value was found in the credentials file - * @throws IOException throws if the provided path can not be found or the file within the pathKey can not be found - */ - private String readValueFromCredentialsFile(String path, String valueKey, String pathKey) throws IOException, CredentialsInFileNotFoundException { - // Read credentials file - String fileContent = new String(Files.readAllBytes(Paths.get(path)), StandardCharsets.UTF_8); - Type credentialsFileType = new TypeToken>(){}.getType(); - Map map = new Gson().fromJson(fileContent, credentialsFileType); - - // Read KEY from credentials file - String key = null; - try { - key = (String) map.get(valueKey); - } catch (ClassCastException ignored) {} - if (key != null && !key.trim().isEmpty()) { - return key; - } - - // Read KEY_PATH from credentials file - String keyPath = null; - try { - keyPath = (String) map.get(pathKey); - } catch (ClassCastException ignored) {} - if (keyPath != null && !keyPath.trim().isEmpty()) { - return new String(Files.readAllBytes(Paths.get(keyPath))); - } - throw new CredentialsInFileNotFoundException("could not find " + valueKey + " or " + pathKey + " in " + path); - } + private final Interceptor authHandler; + private final String defaultCredentialsFilePath = + FileSystemView.getFileSystemView().getHomeDirectory() + + File.separator + + ".stackit" + + File.separator + + "credentials.json"; + + /** + * Set up the KeyFlow Authentication and can be integrated in an OkHttp client, by adding + * `SetupAuth().getAuthHandler()` as interceptor. This relies on the configuration methods via + * ENVs or the credentials file in `$HOME/.stackit/credentials.json` + * + * @throws IOException when a file can be found + * @throws CredentialsInFileNotFoundException when no configuration is set or can be found + * @throws InvalidKeySpecException when the private key can not be parsed + * @throws ApiException when access token creation failed + */ + public SetupAuth() + throws IOException, + InvalidKeySpecException, + CredentialsInFileNotFoundException, + ApiException { + this(new CoreConfiguration.Builder().build()); + } + + /** + * Set up the KeyFlow Authentication and can be integrated in an OkHttp client, by adding + * `SetupAuth().getAuthHandler()` as interceptor. + * + * @param cfg Configuration which describes, which service account and token endpoint should be + * used + * @throws IOException when a file can be found + * @throws CredentialsInFileNotFoundException when no credentials are set or can be found + * @throws InvalidKeySpecException when the private key can not be parsed + * @throws ApiException when access token creation failed + */ + public SetupAuth(CoreConfiguration cfg) + throws IOException, + CredentialsInFileNotFoundException, + InvalidKeySpecException, + ApiException { + if (cfg == null) { + cfg = new CoreConfiguration.Builder().build(); + } + + ServiceAccountKey saKey = setupKeyFlow(cfg); + authHandler = new KeyFlowInterceptor(new KeyFlowAuthenticator(cfg, saKey)); + } + + public Interceptor getAuthHandler() { + return authHandler; + } + + /** + * setupKeyFlow return first found ServiceAccountKey Reads the configured options in the + * following order + * + *
    + *
  1. Explicit configuration in `Configuration` + *
      + *
    • serviceAccountKey + *
    • serviceAccountKeyPath + *
    • credentialsFilePath -> STACKIT_SERVICE_ACCOUNT_KEY / + * STACKIT_SERVICE_ACCOUNT_KEY_PATH + *
    + *
  2. Environment variables + *
      + *
    • STACKIT_SERVICE_ACCOUNT_KEY + *
    • STACKIT_SERVICE_ACCOUNT_KEY_PATH + *
    • STACKIT_CREDENTIALS_PATH -> STACKIT_SERVICE_ACCOUNT_KEY / + * STACKIT_SERVICE_ACCOUNT_KEY_PATH + *
    + *
  3. Credentials file + *
      + *
    • STACKIT_SERVICE_ACCOUNT_KEY + *
    • STACKIT_SERVICE_ACCOUNT_KEY_PATH + *
    + *
+ * + * @param cfg + * @return ServiceAccountKey + * @throws CredentialsInFileNotFoundException thrown when no service account key or private key + * can be found + * @throws IOException thrown when a file can not be found + */ + private ServiceAccountKey setupKeyFlow(CoreConfiguration cfg) + throws CredentialsInFileNotFoundException, IOException { + // Explicit config in code + if (cfg.getServiceAccountKey() != null && !cfg.getServiceAccountKey().trim().isEmpty()) { + ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(cfg.getServiceAccountKey()); + loadPrivateKey(cfg, saKey); + return saKey; + } + + if (cfg.getServiceAccountKeyPath() != null + && !cfg.getServiceAccountKeyPath().trim().isEmpty()) { + String fileContent = + new String( + Files.readAllBytes(Paths.get(cfg.getServiceAccountKeyPath())), + StandardCharsets.UTF_8); + ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(fileContent); + loadPrivateKey(cfg, saKey); + return saKey; + } + + // Env config + if (EnvironmentVariables.STACKIT_SERVICE_ACCOUNT_KEY != null + && !EnvironmentVariables.STACKIT_SERVICE_ACCOUNT_KEY.trim().isEmpty()) { + ServiceAccountKey saKey = + ServiceAccountKey.loadFromJson( + EnvironmentVariables.STACKIT_SERVICE_ACCOUNT_KEY.trim()); + loadPrivateKey(cfg, saKey); + return saKey; + } + + if (EnvironmentVariables.STACKIT_SERVICE_ACCOUNT_KEY_PATH != null + && !EnvironmentVariables.STACKIT_SERVICE_ACCOUNT_KEY_PATH.trim().isEmpty()) { + String fileContent = + new String( + Files.readAllBytes(Paths.get(cfg.getServiceAccountKeyPath())), + StandardCharsets.UTF_8); + ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(fileContent); + loadPrivateKey(cfg, saKey); + return saKey; + } + + if (EnvironmentVariables.STACKIT_CREDENTIALS_PATH != null + && !EnvironmentVariables.STACKIT_CREDENTIALS_PATH.trim().isEmpty()) { + String saKeyJson = + readValueFromCredentialsFile( + EnvironmentVariables.STACKIT_CREDENTIALS_PATH, + EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY, + EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY_PATH); + ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(saKeyJson); + loadPrivateKey(cfg, saKey); + return saKey; + } else { + String saKeyJson = + readValueFromCredentialsFile( + defaultCredentialsFilePath, + EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY, + EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY_PATH); + ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(saKeyJson); + loadPrivateKey(cfg, saKey); + return saKey; + } + } + + private void loadPrivateKey(CoreConfiguration cfg, ServiceAccountKey saKey) + throws PrivateKeyNotFoundException { + if (!saKey.getCredentials().isPrivateKeySet()) { + try { + String privateKey = getPrivateKey(cfg); + saKey.getCredentials().setPrivateKey(privateKey); + } catch (Exception e) { + throw new PrivateKeyNotFoundException("could not find private key", e); + } + } + } + + /** + * Reads the private key in the following order + * + *
    + *
  1. Explicit configuration in `Configuration` + *
      + *
    • privateKey + *
    • privateKeyPath + *
    • credentialsFilePath -> STACKIT_PRIVATE_KEY / STACKIT_PRIVATE_KEY_PATH + *
    + *
  2. Environment variables + *
      + *
    • STACKIT_PRIVATE_KEY + *
    • STACKIT_PRIVATE_KEY_PATH + *
    • STACKIT_CREDENTIALS_PATH -> STACKIT_PRIVATE_KEY / STACKIT_PRIVATE_KEY_PATH + *
    + *
  3. Credentials file + *
      + *
    • STACKIT_PRIVATE_KEY + *
    • STACKIT_PRIVATE_KEY_PATH + *
    + *
+ * + * @param cfg + * @return found private key + * @throws CredentialsInFileNotFoundException throws if no private key could be found + * @throws IOException throws if the provided path can not be found or the file within the + * pathKey can not be found + */ + private String getPrivateKey(CoreConfiguration cfg) + throws CredentialsInFileNotFoundException, IOException { + // Explicit code config + // Set private key + if (cfg.getPrivateKey() != null && !cfg.getPrivateKey().trim().isEmpty()) { + return cfg.getPrivateKey(); + } + // Set private key path + if (cfg.getPrivateKeyPath() != null && !cfg.getPrivateKeyPath().trim().isEmpty()) { + String privateKeyPath = cfg.getPrivateKeyPath(); + return new String( + Files.readAllBytes(Paths.get(privateKeyPath)), StandardCharsets.UTF_8); + } + // Set credentials file + if (cfg.getCredentialsFilePath() != null + && !cfg.getCredentialsFilePath().trim().isEmpty()) { + return readValueFromCredentialsFile( + cfg.getCredentialsFilePath(), + EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY, + EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY_PATH); + } + + // ENVs config + if (EnvironmentVariables.STACKIT_PRIVATE_KEY != null + && !EnvironmentVariables.STACKIT_PRIVATE_KEY.trim().isEmpty()) { + return EnvironmentVariables.STACKIT_PRIVATE_KEY.trim(); + } + if (EnvironmentVariables.STACKIT_PRIVATE_KEY_PATH != null + && !EnvironmentVariables.STACKIT_PRIVATE_KEY_PATH.trim().isEmpty()) { + return new String( + Files.readAllBytes(Paths.get(EnvironmentVariables.STACKIT_PRIVATE_KEY_PATH)), + StandardCharsets.UTF_8); + } + if (EnvironmentVariables.STACKIT_CREDENTIALS_PATH != null + && !EnvironmentVariables.STACKIT_CREDENTIALS_PATH.trim().isEmpty()) { + return readValueFromCredentialsFile( + EnvironmentVariables.STACKIT_CREDENTIALS_PATH, + EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY, + EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY_PATH); + } + + // Read from credentials file in defaultCredentialsFilePath + return readValueFromCredentialsFile( + defaultCredentialsFilePath, + EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY, + EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY_PATH); + } + + /** + * Reads of a json credentials file from `path`, the values of `valueKey` or `pathKey`. + * + * @param path Path of the credentials file which should be read + * @param valueKey key which contains the secret as value + * @param pathKey key which contains a path to a file + * @return Either the value of `valueKey` or the content of the file in `pathKey` + * @throws CredentialsInFileNotFoundException throws if no value was found in the credentials + * file + * @throws IOException throws if the provided path can not be found or the file within the + * pathKey can not be found + */ + private String readValueFromCredentialsFile(String path, String valueKey, String pathKey) + throws IOException, CredentialsInFileNotFoundException { + // Read credentials file + String fileContent = + new String(Files.readAllBytes(Paths.get(path)), StandardCharsets.UTF_8); + Type credentialsFileType = new TypeToken>() {}.getType(); + Map map = new Gson().fromJson(fileContent, credentialsFileType); + + // Read KEY from credentials file + String key = null; + try { + key = (String) map.get(valueKey); + } catch (ClassCastException ignored) { + } + if (key != null && !key.trim().isEmpty()) { + return key; + } + + // Read KEY_PATH from credentials file + String keyPath = null; + try { + keyPath = (String) map.get(pathKey); + } catch (ClassCastException ignored) { + } + if (keyPath != null && !keyPath.trim().isEmpty()) { + return new String(Files.readAllBytes(Paths.get(keyPath))); + } + throw new CredentialsInFileNotFoundException( + "could not find " + valueKey + " or " + pathKey + " in " + path); + } } diff --git a/core/src/main/java/cloud/stackit/sdk/core/config/CoreConfiguration.java b/core/src/main/java/cloud/stackit/sdk/core/config/CoreConfiguration.java index 2579ed8..f100526 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/config/CoreConfiguration.java +++ b/core/src/main/java/cloud/stackit/sdk/core/config/CoreConfiguration.java @@ -3,122 +3,122 @@ import java.util.Map; public class CoreConfiguration { - private final Map defaultHeader; - private final String serviceAccountKey; - private final String serviceAccountKeyPath; - private final String privateKeyPath; - private final String privateKey; - private final String customEndpoint; - private final String credentialsFilePath; - private final String tokenCustomUrl; - private final Long tokenExpirationLeeway; - - CoreConfiguration(Builder builder) { - this.defaultHeader = builder.defaultHeader; - this.serviceAccountKey = builder.serviceAccountKey; - this.serviceAccountKeyPath = builder.serviceAccountKeyPath; - this.privateKeyPath = builder.privateKeyPath; - this.privateKey = builder.privateKey; - this.customEndpoint = builder.customEndpoint; - this.credentialsFilePath = builder.credentialsFilePath; - this.tokenCustomUrl = builder.tokenCustomUrl; - this.tokenExpirationLeeway = builder.tokenExpirationLeeway; - } - - public Map getDefaultHeader() { - return defaultHeader; - } - - public String getServiceAccountKey() { - return serviceAccountKey; - } - - public String getServiceAccountKeyPath() { - return serviceAccountKeyPath; - } - - public String getPrivateKeyPath() { - return privateKeyPath; - } - - public String getPrivateKey() { - return privateKey; - } - - public String getCustomEndpoint() { - return customEndpoint; - } - - public String getCredentialsFilePath() { - return credentialsFilePath; - } - - public String getTokenCustomUrl() { - return tokenCustomUrl; - } - - public Long getTokenExpirationLeeway() { - return tokenExpirationLeeway; - } - - public static class Builder { - private Map defaultHeader; - private String serviceAccountKey; - private String serviceAccountKeyPath; - private String privateKeyPath; - private String privateKey; - private String customEndpoint; - private String credentialsFilePath; - private String tokenCustomUrl; - private Long tokenExpirationLeeway; - - public Builder defaultHeader(Map defaultHeader) { - this.defaultHeader = defaultHeader; - return this; - } - - public Builder serviceAccountKey(String serviceAccountKey) { - this.serviceAccountKey = serviceAccountKey; - return this; - } - - public Builder serviceAccountKeyPath(String serviceAccountKeyPath) { - this.serviceAccountKeyPath = serviceAccountKeyPath; - return this; - } - - public Builder privateKeyPath(String privateKeyPath) { - this.privateKeyPath = privateKeyPath; - return this; - } - - public Builder privateKey(String privateKey) { - this.privateKey = privateKey; - return this; - } - - public Builder customEndpoint(String customEndpoint) { - this.customEndpoint = customEndpoint; - return this; - } - - public Builder credentialsFilePath(String credentialsFilePath) { - this.credentialsFilePath = credentialsFilePath; - return this; - } - - public Builder tokenCustomUrl(String tokenCustomUrl) { - this.tokenCustomUrl = tokenCustomUrl; - return this; - } - - public Builder tokenExpirationLeeway(Long tokenExpirationLeeway) { - this.tokenExpirationLeeway = tokenExpirationLeeway; - return this; - } - - public CoreConfiguration build() { - return new CoreConfiguration(this); - } - } + private final Map defaultHeader; + private final String serviceAccountKey; + private final String serviceAccountKeyPath; + private final String privateKeyPath; + private final String privateKey; + private final String customEndpoint; + private final String credentialsFilePath; + private final String tokenCustomUrl; + private final Long tokenExpirationLeeway; + + CoreConfiguration(Builder builder) { + this.defaultHeader = builder.defaultHeader; + this.serviceAccountKey = builder.serviceAccountKey; + this.serviceAccountKeyPath = builder.serviceAccountKeyPath; + this.privateKeyPath = builder.privateKeyPath; + this.privateKey = builder.privateKey; + this.customEndpoint = builder.customEndpoint; + this.credentialsFilePath = builder.credentialsFilePath; + this.tokenCustomUrl = builder.tokenCustomUrl; + this.tokenExpirationLeeway = builder.tokenExpirationLeeway; + } + + public Map getDefaultHeader() { + return defaultHeader; + } + + public String getServiceAccountKey() { + return serviceAccountKey; + } + + public String getServiceAccountKeyPath() { + return serviceAccountKeyPath; + } + + public String getPrivateKeyPath() { + return privateKeyPath; + } + + public String getPrivateKey() { + return privateKey; + } + + public String getCustomEndpoint() { + return customEndpoint; + } + + public String getCredentialsFilePath() { + return credentialsFilePath; + } + + public String getTokenCustomUrl() { + return tokenCustomUrl; + } + + public Long getTokenExpirationLeeway() { + return tokenExpirationLeeway; + } + + public static class Builder { + private Map defaultHeader; + private String serviceAccountKey; + private String serviceAccountKeyPath; + private String privateKeyPath; + private String privateKey; + private String customEndpoint; + private String credentialsFilePath; + private String tokenCustomUrl; + private Long tokenExpirationLeeway; + + public Builder defaultHeader(Map defaultHeader) { + this.defaultHeader = defaultHeader; + return this; + } + + public Builder serviceAccountKey(String serviceAccountKey) { + this.serviceAccountKey = serviceAccountKey; + return this; + } + + public Builder serviceAccountKeyPath(String serviceAccountKeyPath) { + this.serviceAccountKeyPath = serviceAccountKeyPath; + return this; + } + + public Builder privateKeyPath(String privateKeyPath) { + this.privateKeyPath = privateKeyPath; + return this; + } + + public Builder privateKey(String privateKey) { + this.privateKey = privateKey; + return this; + } + + public Builder customEndpoint(String customEndpoint) { + this.customEndpoint = customEndpoint; + return this; + } + + public Builder credentialsFilePath(String credentialsFilePath) { + this.credentialsFilePath = credentialsFilePath; + return this; + } + + public Builder tokenCustomUrl(String tokenCustomUrl) { + this.tokenCustomUrl = tokenCustomUrl; + return this; + } + + public Builder tokenExpirationLeeway(Long tokenExpirationLeeway) { + this.tokenExpirationLeeway = tokenExpirationLeeway; + return this; + } + + public CoreConfiguration build() { + return new CoreConfiguration(this); + } + } } diff --git a/core/src/main/java/cloud/stackit/sdk/core/config/EnvironmentVariables.java b/core/src/main/java/cloud/stackit/sdk/core/config/EnvironmentVariables.java index 0d54c9f..c005ae5 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/config/EnvironmentVariables.java +++ b/core/src/main/java/cloud/stackit/sdk/core/config/EnvironmentVariables.java @@ -1,30 +1,46 @@ package cloud.stackit.sdk.core.config; public class EnvironmentVariables { - public final static String ENV_STACKIT_SERVICE_ACCOUNT_KEY_PATH = "STACKIT_SERVICE_ACCOUNT_KEY_PATH"; - public final static String ENV_STACKIT_SERVICE_ACCOUNT_KEY = "STACKIT_SERVICE_ACCOUNT_KEY"; - public final static String ENV_STACKIT_PRIVATE_KEY_PATH = "STACKIT_PRIVATE_KEY_PATH"; - public final static String ENV_STACKIT_PRIVATE_KEY = "STACKIT_PRIVATE_KEY"; - public final static String ENV_STACKIT_TOKEN_BASEURL = "STACKIT_TOKEN_BASEURL"; - public final static String ENV_STACKIT_CREDENTIALS_PATH = "STACKIT_CREDENTIALS_PATH"; + public static final String ENV_STACKIT_SERVICE_ACCOUNT_KEY_PATH = + "STACKIT_SERVICE_ACCOUNT_KEY_PATH"; + public static final String ENV_STACKIT_SERVICE_ACCOUNT_KEY = "STACKIT_SERVICE_ACCOUNT_KEY"; + public static final String ENV_STACKIT_PRIVATE_KEY_PATH = "STACKIT_PRIVATE_KEY_PATH"; + public static final String ENV_STACKIT_PRIVATE_KEY = "STACKIT_PRIVATE_KEY"; + public static final String ENV_STACKIT_TOKEN_BASEURL = "STACKIT_TOKEN_BASEURL"; + public static final String ENV_STACKIT_CREDENTIALS_PATH = "STACKIT_CREDENTIALS_PATH"; - public final static String STACKIT_SERVICE_ACCOUNT_KEY_PATH = System.getenv(ENV_STACKIT_SERVICE_ACCOUNT_KEY_PATH); - public final static String STACKIT_SERVICE_ACCOUNT_KEY = System.getenv(ENV_STACKIT_SERVICE_ACCOUNT_KEY); - public final static String STACKIT_PRIVATE_KEY_PATH = System.getenv(ENV_STACKIT_PRIVATE_KEY_PATH); - public final static String STACKIT_PRIVATE_KEY = System.getenv(ENV_STACKIT_PRIVATE_KEY); - public final static String STACKIT_TOKEN_BASEURL = System.getenv(ENV_STACKIT_TOKEN_BASEURL); - public final static String STACKIT_CREDENTIALS_PATH = System.getenv(ENV_STACKIT_CREDENTIALS_PATH); + public static final String STACKIT_SERVICE_ACCOUNT_KEY_PATH = + System.getenv(ENV_STACKIT_SERVICE_ACCOUNT_KEY_PATH); + public static final String STACKIT_SERVICE_ACCOUNT_KEY = + System.getenv(ENV_STACKIT_SERVICE_ACCOUNT_KEY); + public static final String STACKIT_PRIVATE_KEY_PATH = + System.getenv(ENV_STACKIT_PRIVATE_KEY_PATH); + public static final String STACKIT_PRIVATE_KEY = System.getenv(ENV_STACKIT_PRIVATE_KEY); + public static final String STACKIT_TOKEN_BASEURL = System.getenv(ENV_STACKIT_TOKEN_BASEURL); + public static final String STACKIT_CREDENTIALS_PATH = + System.getenv(ENV_STACKIT_CREDENTIALS_PATH); - - @Override - public String toString() { - return "EnvironmentVariables{" + - "STACKIT_SERVICE_ACCOUNT_KEY_PATH='" + STACKIT_SERVICE_ACCOUNT_KEY_PATH + '\'' + - ", STACKIT_SERVICE_ACCOUNT_KEY='" + STACKIT_SERVICE_ACCOUNT_KEY + '\'' + - ", STACKIT_PRIVATE_KEY_PATH='" + STACKIT_PRIVATE_KEY_PATH + '\'' + - ", STACKIT_PRIVATE_KEY='" + STACKIT_PRIVATE_KEY + '\'' + - ", STACKIT_TOKEN_BASEURL='" + STACKIT_TOKEN_BASEURL + '\'' + - ", STACKIT_CREDENTIALS_PATH='" + STACKIT_CREDENTIALS_PATH + '\'' + - '}'; - } + @Override + public String toString() { + return "EnvironmentVariables{" + + "STACKIT_SERVICE_ACCOUNT_KEY_PATH='" + + STACKIT_SERVICE_ACCOUNT_KEY_PATH + + '\'' + + ", STACKIT_SERVICE_ACCOUNT_KEY='" + + STACKIT_SERVICE_ACCOUNT_KEY + + '\'' + + ", STACKIT_PRIVATE_KEY_PATH='" + + STACKIT_PRIVATE_KEY_PATH + + '\'' + + ", STACKIT_PRIVATE_KEY='" + + STACKIT_PRIVATE_KEY + + '\'' + + ", STACKIT_TOKEN_BASEURL='" + + STACKIT_TOKEN_BASEURL + + '\'' + + ", STACKIT_CREDENTIALS_PATH='" + + STACKIT_CREDENTIALS_PATH + + '\'' + + '}'; + } } diff --git a/core/src/main/java/cloud/stackit/sdk/core/exception/ApiException.java b/core/src/main/java/cloud/stackit/sdk/core/exception/ApiException.java index 88f934b..d7d6210 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/exception/ApiException.java +++ b/core/src/main/java/cloud/stackit/sdk/core/exception/ApiException.java @@ -1,152 +1,173 @@ package cloud.stackit.sdk.core.exception; - import java.util.List; import java.util.Map; -/** - *

ApiException class.

- */ +/** ApiException class. */ public class ApiException extends Exception { - private static final long serialVersionUID = 1L; - - private int code = 0; - private Map> responseHeaders = null; - private String responseBody = null; - - /** - *

Constructor for ApiException.

- */ - public ApiException() {} - - /** - *

Constructor for ApiException.

- * - * @param throwable a {@link java.lang.Throwable} object - */ - public ApiException(Throwable throwable) { - super(throwable); - } - - /** - *

Constructor for ApiException.

- * - * @param message the error message - */ - public ApiException(String message) { - super(message); - } - - /** - *

Constructor for ApiException.

- * - * @param message the error message - * @param throwable a {@link java.lang.Throwable} object - * @param code HTTP status code - * @param responseHeaders a {@link java.util.Map} of HTTP response headers - * @param responseBody the response body - */ - public ApiException(String message, Throwable throwable, int code, Map> responseHeaders, String responseBody) { - super(message, throwable); - this.code = code; - this.responseHeaders = responseHeaders; - this.responseBody = responseBody; - } - - /** - *

Constructor for ApiException.

- * - * @param message the error message - * @param code HTTP status code - * @param responseHeaders a {@link java.util.Map} of HTTP response headers - * @param responseBody the response body - */ - public ApiException(String message, int code, Map> responseHeaders, String responseBody) { - this(message, null, code, responseHeaders, responseBody); - } - - /** - *

Constructor for ApiException.

- * - * @param message the error message - * @param throwable a {@link java.lang.Throwable} object - * @param code HTTP status code - * @param responseHeaders a {@link java.util.Map} of HTTP response headers - */ - public ApiException(String message, Throwable throwable, int code, Map> responseHeaders) { - this(message, throwable, code, responseHeaders, null); - } - - /** - *

Constructor for ApiException.

- * - * @param code HTTP status code - * @param responseHeaders a {@link java.util.Map} of HTTP response headers - * @param responseBody the response body - */ - public ApiException(int code, Map> responseHeaders, String responseBody) { - this("Response Code: " + code + " Response Body: " + responseBody, null, code, responseHeaders, responseBody); - } - - /** - *

Constructor for ApiException.

- * - * @param code HTTP status code - * @param message a {@link java.lang.String} object - */ - public ApiException(int code, String message) { - super(message); - this.code = code; - } - - /** - *

Constructor for ApiException.

- * - * @param code HTTP status code - * @param message the error message - * @param responseHeaders a {@link java.util.Map} of HTTP response headers - * @param responseBody the response body - */ - public ApiException(int code, String message, Map> responseHeaders, String responseBody) { - this(code, message); - this.responseHeaders = responseHeaders; - this.responseBody = responseBody; - } - - /** - * Get the HTTP status code. - * - * @return HTTP status code - */ - public int getCode() { - return code; - } - - /** - * Get the HTTP response headers. - * - * @return A map of list of string - */ - public Map> getResponseHeaders() { - return responseHeaders; - } - - /** - * Get the HTTP response body. - * - * @return Response body in the form of string - */ - public String getResponseBody() { - return responseBody; - } - - /** - * Get the exception message including HTTP response data. - * - * @return The exception message - */ - public String getMessage() { - return String.format("Message: %s%nHTTP response code: %s%nHTTP response body: %s%nHTTP response headers: %s", - super.getMessage(), this.getCode(), this.getResponseBody(), this.getResponseHeaders()); - } + private static final long serialVersionUID = 1L; + + private int code = 0; + private Map> responseHeaders = null; + private String responseBody = null; + + /** Constructor for ApiException. */ + public ApiException() {} + + /** + * Constructor for ApiException. + * + * @param throwable a {@link java.lang.Throwable} object + */ + public ApiException(Throwable throwable) { + super(throwable); + } + + /** + * Constructor for ApiException. + * + * @param message the error message + */ + public ApiException(String message) { + super(message); + } + + /** + * Constructor for ApiException. + * + * @param message the error message + * @param throwable a {@link java.lang.Throwable} object + * @param code HTTP status code + * @param responseHeaders a {@link java.util.Map} of HTTP response headers + * @param responseBody the response body + */ + public ApiException( + String message, + Throwable throwable, + int code, + Map> responseHeaders, + String responseBody) { + super(message, throwable); + this.code = code; + this.responseHeaders = responseHeaders; + this.responseBody = responseBody; + } + + /** + * Constructor for ApiException. + * + * @param message the error message + * @param code HTTP status code + * @param responseHeaders a {@link java.util.Map} of HTTP response headers + * @param responseBody the response body + */ + public ApiException( + String message, + int code, + Map> responseHeaders, + String responseBody) { + this(message, null, code, responseHeaders, responseBody); + } + + /** + * Constructor for ApiException. + * + * @param message the error message + * @param throwable a {@link java.lang.Throwable} object + * @param code HTTP status code + * @param responseHeaders a {@link java.util.Map} of HTTP response headers + */ + public ApiException( + String message, + Throwable throwable, + int code, + Map> responseHeaders) { + this(message, throwable, code, responseHeaders, null); + } + + /** + * Constructor for ApiException. + * + * @param code HTTP status code + * @param responseHeaders a {@link java.util.Map} of HTTP response headers + * @param responseBody the response body + */ + public ApiException(int code, Map> responseHeaders, String responseBody) { + this( + "Response Code: " + code + " Response Body: " + responseBody, + null, + code, + responseHeaders, + responseBody); + } + + /** + * Constructor for ApiException. + * + * @param code HTTP status code + * @param message a {@link java.lang.String} object + */ + public ApiException(int code, String message) { + super(message); + this.code = code; + } + + /** + * Constructor for ApiException. + * + * @param code HTTP status code + * @param message the error message + * @param responseHeaders a {@link java.util.Map} of HTTP response headers + * @param responseBody the response body + */ + public ApiException( + int code, + String message, + Map> responseHeaders, + String responseBody) { + this(code, message); + this.responseHeaders = responseHeaders; + this.responseBody = responseBody; + } + + /** + * Get the HTTP status code. + * + * @return HTTP status code + */ + public int getCode() { + return code; + } + + /** + * Get the HTTP response headers. + * + * @return A map of list of string + */ + public Map> getResponseHeaders() { + return responseHeaders; + } + + /** + * Get the HTTP response body. + * + * @return Response body in the form of string + */ + public String getResponseBody() { + return responseBody; + } + + /** + * Get the exception message including HTTP response data. + * + * @return The exception message + */ + public String getMessage() { + return String.format( + "Message: %s%nHTTP response code: %s%nHTTP response body: %s%nHTTP response headers: %s", + super.getMessage(), + this.getCode(), + this.getResponseBody(), + this.getResponseHeaders()); + } } diff --git a/core/src/main/java/cloud/stackit/sdk/core/exception/CredentialsInFileNotFoundException.java b/core/src/main/java/cloud/stackit/sdk/core/exception/CredentialsInFileNotFoundException.java index 75d5e51..052fc01 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/exception/CredentialsInFileNotFoundException.java +++ b/core/src/main/java/cloud/stackit/sdk/core/exception/CredentialsInFileNotFoundException.java @@ -2,11 +2,11 @@ public class CredentialsInFileNotFoundException extends RuntimeException { - public CredentialsInFileNotFoundException(String msg) { - super(msg); - } + public CredentialsInFileNotFoundException(String msg) { + super(msg); + } - public CredentialsInFileNotFoundException(String msg, Throwable cause) { - super(msg, cause); - } + public CredentialsInFileNotFoundException(String msg, Throwable cause) { + super(msg, cause); + } } diff --git a/core/src/main/java/cloud/stackit/sdk/core/exception/PrivateKeyNotFoundException.java b/core/src/main/java/cloud/stackit/sdk/core/exception/PrivateKeyNotFoundException.java index 711eafa..365ea8e 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/exception/PrivateKeyNotFoundException.java +++ b/core/src/main/java/cloud/stackit/sdk/core/exception/PrivateKeyNotFoundException.java @@ -2,11 +2,11 @@ public class PrivateKeyNotFoundException extends RuntimeException { - public PrivateKeyNotFoundException(String msg) { - super(msg); - } + public PrivateKeyNotFoundException(String msg) { + super(msg); + } - public PrivateKeyNotFoundException(String msg, Throwable cause) { - super(msg, cause); - } + public PrivateKeyNotFoundException(String msg, Throwable cause) { + super(msg, cause); + } } diff --git a/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountCredentials.java b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountCredentials.java index b9efc50..83dc2df 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountCredentials.java +++ b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountCredentials.java @@ -6,59 +6,60 @@ import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Base64; -import java.util.UUID; public class ServiceAccountCredentials { - private final String aud; - private final String iss; - private final String kid; - private String privateKey; - private final String sub; + private final String aud; + private final String iss; + private final String kid; + private String privateKey; + private final String sub; - public ServiceAccountCredentials(String aud, String iss, String kid, String privateKey, String sub) { - this.aud = aud; - this.iss = iss; - this.kid = kid; - this.privateKey = privateKey; - this.sub = sub; - } + public ServiceAccountCredentials( + String aud, String iss, String kid, String privateKey, String sub) { + this.aud = aud; + this.iss = iss; + this.kid = kid; + this.privateKey = privateKey; + this.sub = sub; + } - public String getAud() { - return aud; - } + public String getAud() { + return aud; + } - public String getIss() { - return iss; - } + public String getIss() { + return iss; + } - public String getKid() { - return kid; - } + public String getKid() { + return kid; + } - public String getPrivateKey() { - return privateKey; - } + public String getPrivateKey() { + return privateKey; + } - public void setPrivateKey(String privateKey) { - this.privateKey = privateKey; - } + public void setPrivateKey(String privateKey) { + this.privateKey = privateKey; + } - public boolean isPrivateKeySet() { - return privateKey != null && !privateKey.trim().isEmpty(); - } + public boolean isPrivateKeySet() { + return privateKey != null && !privateKey.trim().isEmpty(); + } - public String getSub() { - return sub; - } + public String getSub() { + return sub; + } - public RSAPrivateKey getPrivateKeyParsed() throws NoSuchAlgorithmException, InvalidKeySpecException { - String trimmedKey = privateKey.replaceFirst("-----BEGIN PRIVATE KEY-----", ""); - trimmedKey = trimmedKey.replaceFirst("-----END PRIVATE KEY-----", ""); - trimmedKey = trimmedKey.replaceAll("\n",""); + public RSAPrivateKey getPrivateKeyParsed() + throws NoSuchAlgorithmException, InvalidKeySpecException { + String trimmedKey = privateKey.replaceFirst("-----BEGIN PRIVATE KEY-----", ""); + trimmedKey = trimmedKey.replaceFirst("-----END PRIVATE KEY-----", ""); + trimmedKey = trimmedKey.replaceAll("\n", ""); - byte[] privateBytes = Base64.getDecoder().decode(trimmedKey); - PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateBytes); - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - return (RSAPrivateKey) keyFactory.generatePrivate(keySpec); - } + byte[] privateBytes = Base64.getDecoder().decode(trimmedKey); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + return (RSAPrivateKey) keyFactory.generatePrivate(keySpec); + } } diff --git a/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java index 0ca46d1..bbc7ed1 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java +++ b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java @@ -2,77 +2,86 @@ import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; - import java.util.Date; public class ServiceAccountKey { - private final String id; - private final String publicKey; - private final Date created; - private final String keyType; - private final String keyOrigin; - private final String keyAlgorithm; - private final boolean active; - private final Date validUntil; - private final ServiceAccountCredentials credentials; + private final String id; + private final String publicKey; + private final Date created; + private final String keyType; + private final String keyOrigin; + private final String keyAlgorithm; + private final boolean active; + private final Date validUntil; + private final ServiceAccountCredentials credentials; - public ServiceAccountKey(String id, String publicKey, Date created, String keyType, String keyOrigin, String keyAlgorithm, boolean active, Date validUntil, ServiceAccountCredentials credentials) { - this.id = id; - this.publicKey = publicKey; - this.created = created; - this.keyType = keyType; - this.keyOrigin = keyOrigin; - this.keyAlgorithm = keyAlgorithm; - this.active = active; - this.validUntil = validUntil; - this.credentials = credentials; - } + public ServiceAccountKey( + String id, + String publicKey, + Date created, + String keyType, + String keyOrigin, + String keyAlgorithm, + boolean active, + Date validUntil, + ServiceAccountCredentials credentials) { + this.id = id; + this.publicKey = publicKey; + this.created = created; + this.keyType = keyType; + this.keyOrigin = keyOrigin; + this.keyAlgorithm = keyAlgorithm; + this.active = active; + this.validUntil = validUntil; + this.credentials = credentials; + } - public String getId() { - return id; - } + public String getId() { + return id; + } - public String getPublicKey() { - return publicKey; - } + public String getPublicKey() { + return publicKey; + } - public Date getCreated() { - return created; - } + public Date getCreated() { + return created; + } - public String getKeyType() { - return keyType; - } + public String getKeyType() { + return keyType; + } - public String getKeyOrigin() { - return keyOrigin; - } + public String getKeyOrigin() { + return keyOrigin; + } - public String getKeyAlgorithm() { - return keyAlgorithm; - } + public String getKeyAlgorithm() { + return keyAlgorithm; + } - public boolean isActive() { - return active; - } + public boolean isActive() { + return active; + } - public Date getValidUntil() { - return validUntil; - } + public Date getValidUntil() { + return validUntil; + } - public ServiceAccountCredentials getCredentials() { - return credentials; - } + public ServiceAccountCredentials getCredentials() { + return credentials; + } - public static ServiceAccountKey loadFromJson(String json) throws JsonSyntaxException { - ServiceAccountKey saKey = new Gson().fromJson(json, ServiceAccountKey.class); - if (!saKey.isCredentialsSet()) { - throw new JsonSyntaxException("required field `credentials` in service account key is missing."); - } - return saKey; - } + public static ServiceAccountKey loadFromJson(String json) throws JsonSyntaxException { + ServiceAccountKey saKey = new Gson().fromJson(json, ServiceAccountKey.class); + if (!saKey.isCredentialsSet()) { + throw new JsonSyntaxException( + "required field `credentials` in service account key is missing."); + } + return saKey; + } - private boolean isCredentialsSet() { - return credentials != null; - } + private boolean isCredentialsSet() { + return credentials != null; + } } diff --git a/core/src/test/java/cloud/stackit/sdk/core/config/CoreConfigurationTest.java b/core/src/test/java/cloud/stackit/sdk/core/config/CoreConfigurationTest.java index 70831f8..4c0e37b 100644 --- a/core/src/test/java/cloud/stackit/sdk/core/config/CoreConfigurationTest.java +++ b/core/src/test/java/cloud/stackit/sdk/core/config/CoreConfigurationTest.java @@ -1,200 +1,186 @@ package cloud.stackit.sdk.core.config; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; import java.util.HashMap; import java.util.Map; - -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; class CoreConfigurationTest { - @Test - void getDefaultHeader() { - HashMap map = new HashMap(); - map.put("key", "value"); - CoreConfiguration cfg = - new CoreConfiguration.Builder(). - defaultHeader(map). - build(); - Map cfgHeader = cfg.getDefaultHeader(); + @Test + void getDefaultHeader() { + HashMap map = new HashMap(); + map.put("key", "value"); + CoreConfiguration cfg = new CoreConfiguration.Builder().defaultHeader(map).build(); + Map cfgHeader = cfg.getDefaultHeader(); - assertEquals(map, cfgHeader); - } + assertEquals(map, cfgHeader); + } - @Test - void getServiceAccountKey() { - final String saKey = ""; + @Test + void getServiceAccountKey() { + final String saKey = ""; - CoreConfiguration cfg = new CoreConfiguration.Builder() - .serviceAccountKey(saKey) - .build(); + CoreConfiguration cfg = new CoreConfiguration.Builder().serviceAccountKey(saKey).build(); - String cfgSaKey = cfg.getServiceAccountKey(); + String cfgSaKey = cfg.getServiceAccountKey(); - assertEquals(saKey, cfgSaKey); - } + assertEquals(saKey, cfgSaKey); + } - @Test - void getServiceAccountKeyPath() { - final String saKeyPath = ""; + @Test + void getServiceAccountKeyPath() { + final String saKeyPath = ""; - CoreConfiguration cfg = new CoreConfiguration.Builder() - .serviceAccountKeyPath(saKeyPath) - .build(); + CoreConfiguration cfg = + new CoreConfiguration.Builder().serviceAccountKeyPath(saKeyPath).build(); - String cfgSaKeyPath = cfg.getServiceAccountKeyPath(); + String cfgSaKeyPath = cfg.getServiceAccountKeyPath(); - assertEquals(saKeyPath, cfgSaKeyPath); - } + assertEquals(saKeyPath, cfgSaKeyPath); + } - @Test - void getPrivateKeyPath() { - final String privateKeyPath = ""; + @Test + void getPrivateKeyPath() { + final String privateKeyPath = ""; - CoreConfiguration cfg = new CoreConfiguration.Builder() - .privateKeyPath(privateKeyPath) - .build(); + CoreConfiguration cfg = + new CoreConfiguration.Builder().privateKeyPath(privateKeyPath).build(); - String cfgPrivateKeyPath = cfg.getPrivateKeyPath(); + String cfgPrivateKeyPath = cfg.getPrivateKeyPath(); - assertEquals(privateKeyPath, cfgPrivateKeyPath); - } + assertEquals(privateKeyPath, cfgPrivateKeyPath); + } - @Test - void getPrivateKey() { - final String privateKey = ""; + @Test + void getPrivateKey() { + final String privateKey = ""; - CoreConfiguration cfg = new CoreConfiguration.Builder() - .privateKey(privateKey) - .build(); + CoreConfiguration cfg = new CoreConfiguration.Builder().privateKey(privateKey).build(); - String cfgPrivateKey = cfg.getPrivateKey(); + String cfgPrivateKey = cfg.getPrivateKey(); - assertEquals(privateKey, cfgPrivateKey); - } + assertEquals(privateKey, cfgPrivateKey); + } - @Test - void getCustomEndpoint() { - final String customEndpoint = ""; + @Test + void getCustomEndpoint() { + final String customEndpoint = ""; - CoreConfiguration cfg = new CoreConfiguration.Builder() - .customEndpoint(customEndpoint) - .build(); + CoreConfiguration cfg = + new CoreConfiguration.Builder().customEndpoint(customEndpoint).build(); - String cfgCustomEndpoint = cfg.getCustomEndpoint(); + String cfgCustomEndpoint = cfg.getCustomEndpoint(); - assertEquals(customEndpoint, cfgCustomEndpoint); - } + assertEquals(customEndpoint, cfgCustomEndpoint); + } - @Test - void getCredentialsFilePath() { - final String credFilePath = ""; + @Test + void getCredentialsFilePath() { + final String credFilePath = ""; - CoreConfiguration cfg = new CoreConfiguration.Builder() - .credentialsFilePath(credFilePath) - .build(); + CoreConfiguration cfg = + new CoreConfiguration.Builder().credentialsFilePath(credFilePath).build(); - String cfgCredentialsFilePath = cfg.getCredentialsFilePath(); + String cfgCredentialsFilePath = cfg.getCredentialsFilePath(); - assertEquals(credFilePath, cfgCredentialsFilePath); - } + assertEquals(credFilePath, cfgCredentialsFilePath); + } - @Test - void getTokenCustomUrl() { - final String tokenCustomUrl = ""; + @Test + void getTokenCustomUrl() { + final String tokenCustomUrl = ""; - CoreConfiguration cfg = new CoreConfiguration.Builder() - .tokenCustomUrl(tokenCustomUrl) - .build(); + CoreConfiguration cfg = + new CoreConfiguration.Builder().tokenCustomUrl(tokenCustomUrl).build(); - String cfgTokenUrl = cfg.getTokenCustomUrl(); + String cfgTokenUrl = cfg.getTokenCustomUrl(); - assertEquals(tokenCustomUrl, cfgTokenUrl); - } + assertEquals(tokenCustomUrl, cfgTokenUrl); + } - @Test - void getTokenExpirationLeeway() { - final long tokenExpireLeeway = 100; + @Test + void getTokenExpirationLeeway() { + final long tokenExpireLeeway = 100; - CoreConfiguration cfg = new CoreConfiguration.Builder() - .tokenExpirationLeeway(tokenExpireLeeway) - .build(); + CoreConfiguration cfg = + new CoreConfiguration.Builder().tokenExpirationLeeway(tokenExpireLeeway).build(); - Long cfgTokenExpirationLeeway = cfg.getTokenExpirationLeeway(); + Long cfgTokenExpirationLeeway = cfg.getTokenExpirationLeeway(); - assertEquals(tokenExpireLeeway, cfgTokenExpirationLeeway); - } + assertEquals(tokenExpireLeeway, cfgTokenExpirationLeeway); + } - @Test - void getDefaultHeader_not_set() { - CoreConfiguration cfg = new CoreConfiguration.Builder().build(); - Map defaultHeader = cfg.getDefaultHeader(); + @Test + void getDefaultHeader_not_set() { + CoreConfiguration cfg = new CoreConfiguration.Builder().build(); + Map defaultHeader = cfg.getDefaultHeader(); - assertNull(defaultHeader); - } + assertNull(defaultHeader); + } - @Test - void getServiceAccountKey_not_set() { - CoreConfiguration cfg = new CoreConfiguration.Builder().build(); - String serviceAccountKey = cfg.getServiceAccountKey(); + @Test + void getServiceAccountKey_not_set() { + CoreConfiguration cfg = new CoreConfiguration.Builder().build(); + String serviceAccountKey = cfg.getServiceAccountKey(); - assertNull(serviceAccountKey); - } + assertNull(serviceAccountKey); + } - @Test - void getServiceAccountKeyPath_not_set() { - CoreConfiguration cfg = new CoreConfiguration.Builder().build(); - String serviceAccountKeyPath = cfg.getServiceAccountKeyPath(); + @Test + void getServiceAccountKeyPath_not_set() { + CoreConfiguration cfg = new CoreConfiguration.Builder().build(); + String serviceAccountKeyPath = cfg.getServiceAccountKeyPath(); - assertNull(serviceAccountKeyPath); - } + assertNull(serviceAccountKeyPath); + } - @Test - void getPrivateKeyPath_not_set() { - CoreConfiguration cfg = new CoreConfiguration.Builder().build(); - String privateKeyPath = cfg.getPrivateKeyPath(); + @Test + void getPrivateKeyPath_not_set() { + CoreConfiguration cfg = new CoreConfiguration.Builder().build(); + String privateKeyPath = cfg.getPrivateKeyPath(); - assertNull(privateKeyPath); - } + assertNull(privateKeyPath); + } - @Test - void getPrivateKey_not_set() { - CoreConfiguration cfg = new CoreConfiguration.Builder().build(); - String privateKey = cfg.getPrivateKey(); + @Test + void getPrivateKey_not_set() { + CoreConfiguration cfg = new CoreConfiguration.Builder().build(); + String privateKey = cfg.getPrivateKey(); - assertNull(privateKey); - } + assertNull(privateKey); + } - @Test - void getCustomEndpoint_not_set() { - CoreConfiguration cfg = new CoreConfiguration.Builder().build(); - String customEndpoint = cfg.getCustomEndpoint(); + @Test + void getCustomEndpoint_not_set() { + CoreConfiguration cfg = new CoreConfiguration.Builder().build(); + String customEndpoint = cfg.getCustomEndpoint(); - assertNull(customEndpoint); - } + assertNull(customEndpoint); + } - @Test - void getCredentialsFilePath_not_set() { - CoreConfiguration cfg = new CoreConfiguration.Builder().build(); - String credentialsFilePath = cfg.getCredentialsFilePath(); + @Test + void getCredentialsFilePath_not_set() { + CoreConfiguration cfg = new CoreConfiguration.Builder().build(); + String credentialsFilePath = cfg.getCredentialsFilePath(); - assertNull(credentialsFilePath); - } + assertNull(credentialsFilePath); + } - @Test - void getTokenCustomUrl_not_set() { - CoreConfiguration cfg = new CoreConfiguration.Builder().build(); - String tokenCustomUrl = cfg.getTokenCustomUrl(); + @Test + void getTokenCustomUrl_not_set() { + CoreConfiguration cfg = new CoreConfiguration.Builder().build(); + String tokenCustomUrl = cfg.getTokenCustomUrl(); - assertNull(tokenCustomUrl); - } + assertNull(tokenCustomUrl); + } - @Test - void getTokenExpirationLeeway_not_set() { - CoreConfiguration cfg = new CoreConfiguration.Builder().build(); - Long tokenExpirationLeeway = cfg.getTokenExpirationLeeway(); + @Test + void getTokenExpirationLeeway_not_set() { + CoreConfiguration cfg = new CoreConfiguration.Builder().build(); + Long tokenExpirationLeeway = cfg.getTokenExpirationLeeway(); - assertNull(tokenExpirationLeeway); - } -} \ No newline at end of file + assertNull(tokenExpirationLeeway); + } +} diff --git a/core/src/test/java/cloud/stackit/sdk/core/model/ServiceAccountCredentialsTest.java b/core/src/test/java/cloud/stackit/sdk/core/model/ServiceAccountCredentialsTest.java index 2272cd7..0685ea7 100644 --- a/core/src/test/java/cloud/stackit/sdk/core/model/ServiceAccountCredentialsTest.java +++ b/core/src/test/java/cloud/stackit/sdk/core/model/ServiceAccountCredentialsTest.java @@ -1,84 +1,77 @@ package cloud.stackit.sdk.core.model; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; import java.security.spec.InvalidKeySpecException; - -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; class ServiceAccountCredentialsTest { - @Test - void isPrivateKeySet_null_returnsFalse() { - ServiceAccountCredentials saCreds = new ServiceAccountCredentials(null, null, null, null, null); - - assertFalse( - saCreds.isPrivateKeySet() - ); - } - - @Test - void isPrivateKeySet_emptyString_returnsFalse() { - ServiceAccountCredentials saCreds = new ServiceAccountCredentials(null, null, null, "", null); - - assertFalse( - saCreds.isPrivateKeySet() - ); - } - - @Test - void isPrivateKeySet_emptyStringWhitespaces_returnsFalse() { - ServiceAccountCredentials saCreds = new ServiceAccountCredentials(null, null, null, " ", null); - - assertFalse( - saCreds.isPrivateKeySet() - ); - } - - @Test - void isPrivateKeySet_string_returnsFalse() { - ServiceAccountCredentials saCreds = new ServiceAccountCredentials(null, null, null, "my-private-key", null); - - assertTrue( - saCreds.isPrivateKeySet() - ); - } - - @Test - void getPrivateKeyParsed_notBase64Key_throwsException() { - ServiceAccountCredentials saCreds = new ServiceAccountCredentials(null, null, null, "my-private-key", null); - - assertThrows( - IllegalArgumentException.class, - saCreds::getPrivateKeyParsed - ); - } - - @Test - void getPrivateKeyParsed_invalidKey_throwsException() { - ServiceAccountCredentials saCreds = new ServiceAccountCredentials(null, null, null, "bXktcHJpdmF0ZS1rZXk=", null); - - assertThrows( - InvalidKeySpecException.class, - saCreds::getPrivateKeyParsed - ); - } - - @Test - void getPrivateKeyParsed_validKey_returnsRsaKey() { - final String privateKey = "-----BEGIN PRIVATE KEY-----\n" + - "MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqPfgaTEWEP3S9w0t\n" + - "gsicURfo+nLW09/0KfOPinhYZ4ouzU+3xC4pSlEp8Ut9FgL0AgqNslNaK34Kq+NZ\n" + - "jO9DAQIDAQABAkAgkuLEHLaqkWhLgNKagSajeobLS3rPT0Agm0f7k55FXVt743hw\n" + - "Ngkp98bMNrzy9AQ1mJGbQZGrpr4c8ZAx3aRNAiEAoxK/MgGeeLui385KJ7ZOYktj\n" + - "hLBNAB69fKwTZFsUNh0CIQEJQRpFCcydunv2bENcN/oBTRw39E8GNv2pIcNxZkcb\n" + - "NQIgbYSzn3Py6AasNj6nEtCfB+i1p3F35TK/87DlPSrmAgkCIQDJLhFoj1gbwRbH\n" + - "/bDRPrtlRUDDx44wHoEhSDRdy77eiQIgE6z/k6I+ChN1LLttwX0galITxmAYrOBh\n" + - "BVl433tgTTQ=\n" + - "-----END PRIVATE KEY-----"; - - ServiceAccountCredentials saCreds = new ServiceAccountCredentials(null, null, null, privateKey, null); - - assertDoesNotThrow(saCreds::getPrivateKeyParsed); - } -} \ No newline at end of file + @Test + void isPrivateKeySet_null_returnsFalse() { + ServiceAccountCredentials saCreds = + new ServiceAccountCredentials(null, null, null, null, null); + + assertFalse(saCreds.isPrivateKeySet()); + } + + @Test + void isPrivateKeySet_emptyString_returnsFalse() { + ServiceAccountCredentials saCreds = + new ServiceAccountCredentials(null, null, null, "", null); + + assertFalse(saCreds.isPrivateKeySet()); + } + + @Test + void isPrivateKeySet_emptyStringWhitespaces_returnsFalse() { + ServiceAccountCredentials saCreds = + new ServiceAccountCredentials(null, null, null, " ", null); + + assertFalse(saCreds.isPrivateKeySet()); + } + + @Test + void isPrivateKeySet_string_returnsFalse() { + ServiceAccountCredentials saCreds = + new ServiceAccountCredentials(null, null, null, "my-private-key", null); + + assertTrue(saCreds.isPrivateKeySet()); + } + + @Test + void getPrivateKeyParsed_notBase64Key_throwsException() { + ServiceAccountCredentials saCreds = + new ServiceAccountCredentials(null, null, null, "my-private-key", null); + + assertThrows(IllegalArgumentException.class, saCreds::getPrivateKeyParsed); + } + + @Test + void getPrivateKeyParsed_invalidKey_throwsException() { + ServiceAccountCredentials saCreds = + new ServiceAccountCredentials(null, null, null, "bXktcHJpdmF0ZS1rZXk=", null); + + assertThrows(InvalidKeySpecException.class, saCreds::getPrivateKeyParsed); + } + + @Test + void getPrivateKeyParsed_validKey_returnsRsaKey() { + final String privateKey = + "-----BEGIN PRIVATE KEY-----\n" + + "MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqPfgaTEWEP3S9w0t\n" + + "gsicURfo+nLW09/0KfOPinhYZ4ouzU+3xC4pSlEp8Ut9FgL0AgqNslNaK34Kq+NZ\n" + + "jO9DAQIDAQABAkAgkuLEHLaqkWhLgNKagSajeobLS3rPT0Agm0f7k55FXVt743hw\n" + + "Ngkp98bMNrzy9AQ1mJGbQZGrpr4c8ZAx3aRNAiEAoxK/MgGeeLui385KJ7ZOYktj\n" + + "hLBNAB69fKwTZFsUNh0CIQEJQRpFCcydunv2bENcN/oBTRw39E8GNv2pIcNxZkcb\n" + + "NQIgbYSzn3Py6AasNj6nEtCfB+i1p3F35TK/87DlPSrmAgkCIQDJLhFoj1gbwRbH\n" + + "/bDRPrtlRUDDx44wHoEhSDRdy77eiQIgE6z/k6I+ChN1LLttwX0galITxmAYrOBh\n" + + "BVl433tgTTQ=\n" + + "-----END PRIVATE KEY-----"; + + ServiceAccountCredentials saCreds = + new ServiceAccountCredentials(null, null, null, privateKey, null); + + assertDoesNotThrow(saCreds::getPrivateKeyParsed); + } +} diff --git a/core/src/test/java/cloud/stackit/sdk/core/model/ServiceAccountKeyTest.java b/core/src/test/java/cloud/stackit/sdk/core/model/ServiceAccountKeyTest.java index 0ff4030..2909742 100644 --- a/core/src/test/java/cloud/stackit/sdk/core/model/ServiceAccountKeyTest.java +++ b/core/src/test/java/cloud/stackit/sdk/core/model/ServiceAccountKeyTest.java @@ -1,105 +1,127 @@ package cloud.stackit.sdk.core.model; +import static org.junit.jupiter.api.Assertions.*; + import com.google.gson.JsonSyntaxException; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; - class ServiceAccountKeyTest { - @Test - void loadFromJson_validJson_returnSaKey() { - final String uuid = "6d778bbf-6c86-46e6-952a-0c1b5fd87be3"; - final String iss = "service-account-test@sa.stackit.cloud"; - final String aud = "https://aud.stackit.cloud"; - - final String privateKey = "-----BEGIN PRIVATE KEY-----\n" + - "MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqPfgaTEWEP3S9w0t\n" + - "gsicURfo+nLW09/0KfOPinhYZ4ouzU+3xC4pSlEp8Ut9FgL0AgqNslNaK34Kq+NZ\n" + - "jO9DAQIDAQABAkAgkuLEHLaqkWhLgNKagSajeobLS3rPT0Agm0f7k55FXVt743hw\n" + - "Ngkp98bMNrzy9AQ1mJGbQZGrpr4c8ZAx3aRNAiEAoxK/MgGeeLui385KJ7ZOYktj\n" + - "hLBNAB69fKwTZFsUNh0CIQEJQRpFCcydunv2bENcN/oBTRw39E8GNv2pIcNxZkcb\n" + - "NQIgbYSzn3Py6AasNj6nEtCfB+i1p3F35TK/87DlPSrmAgkCIQDJLhFoj1gbwRbH\n" + - "/bDRPrtlRUDDx44wHoEhSDRdy77eiQIgE6z/k6I+ChN1LLttwX0galITxmAYrOBh\n" + - "BVl433tgTTQ=\n" + - "-----END PRIVATE KEY-----"; + @Test + void loadFromJson_validJson_returnSaKey() { + final String uuid = "6d778bbf-6c86-46e6-952a-0c1b5fd87be3"; + final String iss = "service-account-test@sa.stackit.cloud"; + final String aud = "https://aud.stackit.cloud"; - final String jsonContent = "{\n" + - " \"id\": \"" + uuid + "\",\n" + - " \"publicKey\": \"-----BEGIN PUBLIC KEY-----\\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf\\n9Cnzj4p4WGeKLs1Pt8QuKUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQ==\\n-----END PUBLIC KEY-----\",\n" + - " \"createdAt\": \"2025-01-01T01:00:00.000+00:00\",\n" + - " \"keyType\": \"USER_MANAGED\",\n" + - " \"keyOrigin\": \"GENERATED\",\n" + - " \"keyAlgorithm\": \"RSA_2048\",\n" + - " \"active\": true,\n" + - " \"credentials\": {\n" + - " \"kid\": \"" + uuid + "\",\n" + - " \"iss\": \"" + iss + "\",\n" + - " \"sub\": \"" + uuid + "\",\n" + - " \"aud\": \"" + aud + "\",\n" + - " \"privateKey\": \"" + privateKey + "\"\n" + - " }\n" + - "}\n"; + final String privateKey = + "-----BEGIN PRIVATE KEY-----\n" + + "MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqPfgaTEWEP3S9w0t\n" + + "gsicURfo+nLW09/0KfOPinhYZ4ouzU+3xC4pSlEp8Ut9FgL0AgqNslNaK34Kq+NZ\n" + + "jO9DAQIDAQABAkAgkuLEHLaqkWhLgNKagSajeobLS3rPT0Agm0f7k55FXVt743hw\n" + + "Ngkp98bMNrzy9AQ1mJGbQZGrpr4c8ZAx3aRNAiEAoxK/MgGeeLui385KJ7ZOYktj\n" + + "hLBNAB69fKwTZFsUNh0CIQEJQRpFCcydunv2bENcN/oBTRw39E8GNv2pIcNxZkcb\n" + + "NQIgbYSzn3Py6AasNj6nEtCfB+i1p3F35TK/87DlPSrmAgkCIQDJLhFoj1gbwRbH\n" + + "/bDRPrtlRUDDx44wHoEhSDRdy77eiQIgE6z/k6I+ChN1LLttwX0galITxmAYrOBh\n" + + "BVl433tgTTQ=\n" + + "-----END PRIVATE KEY-----"; - assertDoesNotThrow(() -> ServiceAccountKey.loadFromJson(jsonContent)); - ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(jsonContent); + final String jsonContent = + "{\n" + + " \"id\": \"" + + uuid + + "\",\n" + + " \"publicKey\": \"-----BEGIN PUBLIC KEY-----\\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf\\n9Cnzj4p4WGeKLs1Pt8QuKUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQ==\\n-----END PUBLIC KEY-----\",\n" + + " \"createdAt\": \"2025-01-01T01:00:00.000+00:00\",\n" + + " \"keyType\": \"USER_MANAGED\",\n" + + " \"keyOrigin\": \"GENERATED\",\n" + + " \"keyAlgorithm\": \"RSA_2048\",\n" + + " \"active\": true,\n" + + " \"credentials\": {\n" + + " \"kid\": \"" + + uuid + + "\",\n" + + " \"iss\": \"" + + iss + + "\",\n" + + " \"sub\": \"" + + uuid + + "\",\n" + + " \"aud\": \"" + + aud + + "\",\n" + + " \"privateKey\": \"" + + privateKey + + "\"\n" + + " }\n" + + "}\n"; - assertEquals(uuid, saKey.getId()); - assertEquals(uuid, saKey.getCredentials().getKid()); - assertEquals(iss, saKey.getCredentials().getIss()); - assertEquals(uuid, saKey.getCredentials().getSub()); - assertEquals(aud, saKey.getCredentials().getAud()); - assertEquals(privateKey, saKey.getCredentials().getPrivateKey()); - } + assertDoesNotThrow(() -> ServiceAccountKey.loadFromJson(jsonContent)); + ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(jsonContent); - @Test - void loadFromJson_validJsonWithoutCredentials_throwsException() { - final String jsonContent = "{\n" + - " \"id\": \"6d778bbf-6c86-46e6-952a-0c1b5fd87be3\",\n" + - " \"publicKey\": \"-----BEGIN PUBLIC KEY-----\\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf\\n9Cnzj4p4WGeKLs1Pt8QuKUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQ==\\n-----END PUBLIC KEY-----\",\n" + - " \"createdAt\": \"2025-01-01T01:00:00.000+00:00\",\n" + - " \"keyType\": \"USER_MANAGED\",\n" + - " \"keyOrigin\": \"GENERATED\",\n" + - " \"keyAlgorithm\": \"RSA_2048\",\n" + - " \"active\": true\n" + - "}\n"; + assertEquals(uuid, saKey.getId()); + assertEquals(uuid, saKey.getCredentials().getKid()); + assertEquals(iss, saKey.getCredentials().getIss()); + assertEquals(uuid, saKey.getCredentials().getSub()); + assertEquals(aud, saKey.getCredentials().getAud()); + assertEquals(privateKey, saKey.getCredentials().getPrivateKey()); + } - assertThrows( - JsonSyntaxException.class, - () -> ServiceAccountKey.loadFromJson(jsonContent) - ); - } + @Test + void loadFromJson_validJsonWithoutCredentials_throwsException() { + final String jsonContent = + "{\n" + + " \"id\": \"6d778bbf-6c86-46e6-952a-0c1b5fd87be3\",\n" + + " \"publicKey\": \"-----BEGIN PUBLIC KEY-----\\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf\\n9Cnzj4p4WGeKLs1Pt8QuKUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQ==\\n-----END PUBLIC KEY-----\",\n" + + " \"createdAt\": \"2025-01-01T01:00:00.000+00:00\",\n" + + " \"keyType\": \"USER_MANAGED\",\n" + + " \"keyOrigin\": \"GENERATED\",\n" + + " \"keyAlgorithm\": \"RSA_2048\",\n" + + " \"active\": true\n" + + "}\n"; - @Test - void loadFromJson_validJsonWithoutPrivateKey_returnSaKey() { - final String uuid = "6d778bbf-6c86-46e6-952a-0c1b5fd87be3"; - final String iss = "service-account-test@sa.stackit.cloud"; - final String aud = "https://aud.stackit.cloud"; + assertThrows(JsonSyntaxException.class, () -> ServiceAccountKey.loadFromJson(jsonContent)); + } - final String jsonContent = "{\n" + - " \"id\": \"" + uuid + "\",\n" + - " \"publicKey\": \"-----BEGIN PUBLIC KEY-----\\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf\\n9Cnzj4p4WGeKLs1Pt8QuKUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQ==\\n-----END PUBLIC KEY-----\",\n" + - " \"createdAt\": \"2025-01-01T01:00:00.000+00:00\",\n" + - " \"keyType\": \"USER_MANAGED\",\n" + - " \"keyOrigin\": \"GENERATED\",\n" + - " \"keyAlgorithm\": \"RSA_2048\",\n" + - " \"active\": true,\n" + - " \"credentials\": {\n" + - " \"kid\": \"" + uuid + "\",\n" + - " \"iss\": \"" + iss + "\",\n" + - " \"sub\": \"" + uuid + "\",\n" + - " \"aud\": \"" + aud + "\"\n" + - " }\n" + - "}\n"; + @Test + void loadFromJson_validJsonWithoutPrivateKey_returnSaKey() { + final String uuid = "6d778bbf-6c86-46e6-952a-0c1b5fd87be3"; + final String iss = "service-account-test@sa.stackit.cloud"; + final String aud = "https://aud.stackit.cloud"; - assertDoesNotThrow(() -> ServiceAccountKey.loadFromJson(jsonContent)); - ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(jsonContent); + final String jsonContent = + "{\n" + + " \"id\": \"" + + uuid + + "\",\n" + + " \"publicKey\": \"-----BEGIN PUBLIC KEY-----\\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf\\n9Cnzj4p4WGeKLs1Pt8QuKUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQ==\\n-----END PUBLIC KEY-----\",\n" + + " \"createdAt\": \"2025-01-01T01:00:00.000+00:00\",\n" + + " \"keyType\": \"USER_MANAGED\",\n" + + " \"keyOrigin\": \"GENERATED\",\n" + + " \"keyAlgorithm\": \"RSA_2048\",\n" + + " \"active\": true,\n" + + " \"credentials\": {\n" + + " \"kid\": \"" + + uuid + + "\",\n" + + " \"iss\": \"" + + iss + + "\",\n" + + " \"sub\": \"" + + uuid + + "\",\n" + + " \"aud\": \"" + + aud + + "\"\n" + + " }\n" + + "}\n"; - assertEquals(uuid, saKey.getId()); - assertEquals(uuid, saKey.getCredentials().getKid()); - assertEquals(iss, saKey.getCredentials().getIss()); - assertEquals(uuid, saKey.getCredentials().getSub()); - assertEquals(aud, saKey.getCredentials().getAud()); - } + assertDoesNotThrow(() -> ServiceAccountKey.loadFromJson(jsonContent)); + ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(jsonContent); -} \ No newline at end of file + assertEquals(uuid, saKey.getId()); + assertEquals(uuid, saKey.getCredentials().getKid()); + assertEquals(iss, saKey.getCredentials().getIss()); + assertEquals(uuid, saKey.getCredentials().getSub()); + assertEquals(aud, saKey.getCredentials().getAud()); + } +} diff --git a/examples/authentication/build.gradle b/examples/authentication/build.gradle new file mode 100644 index 0000000..1518830 --- /dev/null +++ b/examples/authentication/build.gradle @@ -0,0 +1,3 @@ +dependencies { + implementation project (':services:resourcemanager') +} diff --git a/examples/authentication/src/main/java/cloud/stackit/sdk/authentication/examples/AuthenticationExample.java b/examples/authentication/src/main/java/cloud/stackit/sdk/authentication/examples/AuthenticationExample.java new file mode 100644 index 0000000..cc10d65 --- /dev/null +++ b/examples/authentication/src/main/java/cloud/stackit/sdk/authentication/examples/AuthenticationExample.java @@ -0,0 +1,29 @@ +package cloud.stackit.sdk.authentication.examples; + +import cloud.stackit.sdk.core.config.CoreConfiguration; +import cloud.stackit.sdk.resourcemanager.api.DefaultApi; +import cloud.stackit.sdk.resourcemanager.model.ListOrganizationsResponse; + +class AuthenticationExample { + public static void main(String[] args) { + String SERVICE_ACCOUNT_KEY_PATH = "/path/to/your/sa/key.json"; + String SERIVCE_ACCOUNT_MAIL = "name-1234@sa.stackit.cloud"; + + CoreConfiguration config = + new CoreConfiguration.Builder() + .serviceAccountKeyPath(SERVICE_ACCOUNT_KEY_PATH) + .build(); + + try { + DefaultApi api = new DefaultApi(config); + + /* list all organizations */ + ListOrganizationsResponse response = + api.listOrganizations(null, SERIVCE_ACCOUNT_MAIL, null, null, null); + + System.out.println(response); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/services/resourcemanager/build.gradle b/services/resourcemanager/build.gradle index 10cd648..ec484aa 100644 --- a/services/resourcemanager/build.gradle +++ b/services/resourcemanager/build.gradle @@ -4,6 +4,7 @@ ext { } dependencies { + implementation project (':core') implementation "com.google.code.findbugs:jsr305:3.0.2" implementation 'com.squareup.okhttp3:okhttp:4.12.0' implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0' diff --git a/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/ApiClient.java b/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/ApiClient.java index 082b248..8fd8817 100644 --- a/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/ApiClient.java +++ b/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/ApiClient.java @@ -12,6 +12,8 @@ package cloud.stackit.sdk.resourcemanager; +import cloud.stackit.sdk.core.auth.SetupAuth; +import cloud.stackit.sdk.core.config.CoreConfiguration; import cloud.stackit.sdk.resourcemanager.auth.ApiKeyAuth; import cloud.stackit.sdk.resourcemanager.auth.Authentication; import cloud.stackit.sdk.resourcemanager.auth.HttpBasicAuth; @@ -31,6 +33,7 @@ import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; +import java.security.spec.InvalidKeySpecException; import java.text.DateFormat; import java.time.LocalDate; import java.time.OffsetDateTime; @@ -118,25 +121,28 @@ public ApiClient(OkHttpClient client) { authentications = Collections.unmodifiableMap(authentications); } - public ApiClient(CoreConfiguration config) throws IOException, InvalidKeySpecException, cloud.stackit.sdk.core.exception.ApiException { - init(); - - if (config.getCustomEndpoint() != null && !config.getCustomEndpoint().trim().isEmpty()) { - basePath = config.getCustomEndpoint(); - } - if (config.getDefaultHeader() != null) { - defaultHeaderMap = config.getDefaultHeader(); - } - SetupAuth auth; - auth = new SetupAuth(config); - List interceptors = new LinkedList<>(); - interceptors.add(auth.getAuthHandler()); - initHttpClient(interceptors); - } - - protected void initHttpClient() { - initHttpClient(Collections.emptyList()); - } + public ApiClient(CoreConfiguration config) + throws IOException, + InvalidKeySpecException, + cloud.stackit.sdk.core.exception.ApiException { + init(); + + if (config.getCustomEndpoint() != null && !config.getCustomEndpoint().trim().isEmpty()) { + basePath = config.getCustomEndpoint(); + } + if (config.getDefaultHeader() != null) { + defaultHeaderMap = config.getDefaultHeader(); + } + SetupAuth auth; + auth = new SetupAuth(config); + List interceptors = new LinkedList<>(); + interceptors.add(auth.getAuthHandler()); + initHttpClient(interceptors); + } + + protected void initHttpClient() { + initHttpClient(Collections.emptyList()); + } protected void initHttpClient(List interceptors) { OkHttpClient.Builder builder = new OkHttpClient.Builder(); diff --git a/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/api/DefaultApi.java b/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/api/DefaultApi.java index c3769bd..3a78097 100644 --- a/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/api/DefaultApi.java +++ b/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/api/DefaultApi.java @@ -27,19 +27,16 @@ import cloud.stackit.sdk.resourcemanager.model.ListFoldersResponse; import cloud.stackit.sdk.resourcemanager.model.ListOrganizationsResponse; import cloud.stackit.sdk.resourcemanager.model.ListProjectsResponse; - -import java.security.spec.InvalidKeySpecException; -import java.time.OffsetDateTime; import cloud.stackit.sdk.resourcemanager.model.OrganizationResponse; import cloud.stackit.sdk.resourcemanager.model.PartialUpdateFolderPayload; import cloud.stackit.sdk.resourcemanager.model.PartialUpdateOrganizationPayload; import cloud.stackit.sdk.resourcemanager.model.PartialUpdateProjectPayload; import cloud.stackit.sdk.resourcemanager.model.Project; import com.google.gson.reflect.TypeToken; - -import javax.security.auth.login.CredentialNotFoundException; +import java.io.IOException; import java.lang.reflect.Type; import java.math.BigDecimal; +import java.security.spec.InvalidKeySpecException; import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.HashMap; @@ -59,17 +56,21 @@ public DefaultApi(ApiClient apiClient) { this.localVarApiClient = apiClient; } - // TODO: remove in follow up story the service specific ApiException and use instead the ApiException of core - public DefaultApi(CoreConfiguration config) throws IOException, InvalidKeySpecException, cloud.stackit.sdk.core.exception.ApiException { - if (config.getCustomEndpoint() != null && !config.getCustomEndpoint().trim().isEmpty()) { - localCustomBaseUrl = config.getCustomEndpoint(); - } - this.localVarApiClient = new ApiClient(config); - } + // TODO: remove in follow up story the service specific ApiException and use instead the + // ApiException of core + public DefaultApi(CoreConfiguration config) + throws InvalidKeySpecException, + cloud.stackit.sdk.core.exception.ApiException, + IOException { + if (config.getCustomEndpoint() != null && !config.getCustomEndpoint().trim().isEmpty()) { + localCustomBaseUrl = config.getCustomEndpoint(); + } + this.localVarApiClient = new ApiClient(config); + } - public ApiClient getApiClient() { - return localVarApiClient; - } + public ApiClient getApiClient() { + return localVarApiClient; + } public void setApiClient(ApiClient apiClient) { this.localVarApiClient = apiClient; diff --git a/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/main.java b/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/main.java deleted file mode 100644 index 8a78743..0000000 --- a/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/main.java +++ /dev/null @@ -1,26 +0,0 @@ -package cloud.stackit.sdk.resourcemanager; - -import cloud.stackit.sdk.core.config.CoreConfiguration; -import cloud.stackit.sdk.resourcemanager.api.DefaultApi; -import cloud.stackit.sdk.resourcemanager.model.ListOrganizationsResponse; - -import javax.security.auth.login.CredentialNotFoundException; -import java.io.IOException; -import java.security.spec.InvalidKeySpecException; - -public class main { - public static void main(String[] args) throws IOException, InvalidKeySpecException, ApiException, cloud.stackit.sdk.core.exception.ApiException { - String SERVICE_ACCOUNT_KEY_PATH = "/path/to/your/sa/key.json"; - String SERIVCE_ACCOUNT_MAIL = "name-1234@sa.stackit.cloud"; - - CoreConfiguration config = new CoreConfiguration - .Builder() - .serviceAccountKeyPath(SERVICE_ACCOUNT_KEY_PATH) - .build(); - DefaultApi api = new DefaultApi(config); - - ListOrganizationsResponse response = api.listOrganizations(null, SERIVCE_ACCOUNT_MAIL, null, null, null); - - System.out.println(response); - } -} From 6a60e50a5fe2e60d8302b70b70df5e0f4248c53e Mon Sep 17 00:00:00 2001 From: Ruben Hoenle Date: Mon, 4 Aug 2025 10:28:46 +0200 Subject: [PATCH 08/12] feat(deps): apply junit dependencies all gradle subprojects --- build.gradle | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 2ebfd32..70a21c3 100644 --- a/build.gradle +++ b/build.gradle @@ -24,7 +24,7 @@ allprojects { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 - tasks.withType(JavaCompile) { + tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' } @@ -120,7 +120,7 @@ subprojects { } } - tasks.withType(Test) { + tasks.withType(Test).configureEach { // Enable JUnit 5 (Gradle 4.6+). useJUnitPlatform() @@ -138,5 +138,8 @@ subprojects { // prevent circular dependency implementation project(':core') } + + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.0' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.11.0' } } From 24184473ea3fefdefdd7598d5e904886d48e3d29 Mon Sep 17 00:00:00 2001 From: Marcel Jacek Date: Tue, 5 Aug 2025 16:09:46 +0200 Subject: [PATCH 09/12] add unit tests for authentication --- core/build.gradle | 4 + .../sdk/core/KeyFlowAuthenticator.java | 82 ++- .../stackit/sdk/core/KeyFlowInterceptor.java | 4 +- .../stackit/sdk/core/auth/SetupAuth.java | 143 +++--- .../sdk/core/config/EnvironmentVariables.java | 53 +- .../core/model/ServiceAccountCredentials.java | 21 +- .../sdk/core/model/ServiceAccountKey.java | 31 ++ .../cloud/stackit/sdk/core/utils/Utils.java | 7 + .../sdk/core/KeyFlowAuthenticatorTest.java | 267 ++++++++++ .../sdk/core/KeyFlowInterceptorTest.java | 62 +++ .../stackit/sdk/core/auth/SetupAuthTest.java | 481 ++++++++++++++++++ .../stackit/sdk/core/utils/UtilsTest.java | 43 ++ .../sdk/resourcemanager/ApiClient.java | 1 + 13 files changed, 1064 insertions(+), 135 deletions(-) create mode 100644 core/src/main/java/cloud/stackit/sdk/core/utils/Utils.java create mode 100644 core/src/test/java/cloud/stackit/sdk/core/KeyFlowAuthenticatorTest.java create mode 100644 core/src/test/java/cloud/stackit/sdk/core/KeyFlowInterceptorTest.java create mode 100644 core/src/test/java/cloud/stackit/sdk/core/auth/SetupAuthTest.java create mode 100644 core/src/test/java/cloud/stackit/sdk/core/utils/UtilsTest.java diff --git a/core/build.gradle b/core/build.gradle index 9e58e22..55c170e 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -3,4 +3,8 @@ dependencies { implementation 'com.auth0:java-jwt:4.5.0' implementation 'com.squareup.okhttp3:okhttp:4.12.0' implementation 'com.google.code.gson:gson:2.9.1' + + testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.0' + testImplementation 'org.mockito:mockito-core:5.18.0' + testImplementation 'org.mockito:mockito-junit-jupiter:5.18.0' } diff --git a/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java index 97d51ae..21170ac 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java +++ b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java @@ -1,8 +1,10 @@ package cloud.stackit.sdk.core; import cloud.stackit.sdk.core.config.CoreConfiguration; +import cloud.stackit.sdk.core.config.EnvironmentVariables; import cloud.stackit.sdk.core.exception.ApiException; import cloud.stackit.sdk.core.model.ServiceAccountKey; +import cloud.stackit.sdk.core.utils.Utils; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import com.google.gson.Gson; @@ -36,7 +38,7 @@ public class KeyFlowAuthenticator { private final String tokenUrl; private long tokenLeewayInSeconds = DEFAULT_TOKEN_LEEWAY; - private static class KeyFlowTokenResponse { + protected static class KeyFlowTokenResponse { @SerializedName("access_token") private String accessToken; @@ -52,27 +54,42 @@ private static class KeyFlowTokenResponse { @SerializedName("token_type") private String tokenType; - public boolean isExpired() { + public KeyFlowTokenResponse( + String accessToken, + String refreshToken, + long expiresIn, + String scope, + String tokenType) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + this.expiresIn = expiresIn; + this.scope = scope; + this.tokenType = tokenType; + } + + protected boolean isExpired() { return expiresIn < new Date().toInstant().getEpochSecond(); } - public String getAccessToken() { + protected String getAccessToken() { return accessToken; } } + public KeyFlowAuthenticator(CoreConfiguration cfg, ServiceAccountKey saKey) { + this(cfg, saKey, null); + } + /** * Creates the initial service account and refreshes expired access token. * * @param cfg Configuration to set a custom token endpoint and the token expiration leeway. * @param saKey Service Account Key, which should be used for the authentication - * @throws InvalidKeySpecException thrown when the private key in the service account can not be - * parsed - * @throws IOException thrown on unexpected responses from the key flow - * @throws ApiException thrown on unexpected responses from the key flow */ - public KeyFlowAuthenticator(CoreConfiguration cfg, ServiceAccountKey saKey) - throws InvalidKeySpecException, IOException, ApiException { + public KeyFlowAuthenticator( + CoreConfiguration cfg, + ServiceAccountKey saKey, + EnvironmentVariables environmentVariables) { this.saKey = saKey; this.gson = new Gson(); this.httpClient = @@ -81,26 +98,36 @@ public KeyFlowAuthenticator(CoreConfiguration cfg, ServiceAccountKey saKey) .writeTimeout(10, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build(); - if (cfg.getTokenCustomUrl() != null && !cfg.getTokenCustomUrl().trim().isEmpty()) { + + if (environmentVariables == null) { + environmentVariables = new EnvironmentVariables(); + } + + if (Utils.isStringSet(cfg.getTokenCustomUrl())) { this.tokenUrl = cfg.getTokenCustomUrl(); + } else if (Utils.isStringSet(environmentVariables.getStackitTokenBaseurl())) { + this.tokenUrl = environmentVariables.getStackitTokenBaseurl(); } else { this.tokenUrl = DEFAULT_TOKEN_ENDPOINT; } if (cfg.getTokenExpirationLeeway() != null && cfg.getTokenExpirationLeeway() > 0) { this.tokenLeewayInSeconds = cfg.getTokenExpirationLeeway(); } - - createAccessToken(); } /** * Returns access token. If the token is expired it creates a new token. * + * @throws InvalidKeySpecException thrown when the private key in the service account can not be + * parsed * @throws IOException request for new access token failed * @throws ApiException response for new access token with bad status code */ - public synchronized String getAccessToken() throws IOException, ApiException { - if (token == null || token.isExpired()) { + public synchronized String getAccessToken() + throws IOException, ApiException, InvalidKeySpecException { + if (token == null) { + createAccessToken(); + } else if (token.isExpired()) { createAccessTokenWithRefreshToken(); } return token.getAccessToken(); @@ -114,7 +141,7 @@ public synchronized String getAccessToken() throws IOException, ApiException { * @throws ApiException response for new access token with bad status code * @throws JsonSyntaxException parsing of the created access token failed */ - private void createAccessToken() + protected void createAccessToken() throws InvalidKeySpecException, IOException, JsonSyntaxException, ApiException { String grant = "urn:ietf:params:oauth:grant-type:jwt-bearer"; String assertion; @@ -137,7 +164,7 @@ private void createAccessToken() * @throws ApiException response for new access token with bad status code * @throws JsonSyntaxException can not parse new access token */ - private synchronized void createAccessTokenWithRefreshToken() + protected synchronized void createAccessTokenWithRefreshToken() throws IOException, JsonSyntaxException, ApiException { String refreshToken = token.refreshToken; Response response = requestToken(REFRESH_TOKEN, refreshToken).execute(); @@ -146,7 +173,7 @@ private synchronized void createAccessTokenWithRefreshToken() } private synchronized void parseTokenResponse(Response response) - throws ApiException, JsonSyntaxException { + throws ApiException, JsonSyntaxException, IOException { if (response.code() != HttpURLConnection.HTTP_OK) { String body = null; if (response.body() != null) { @@ -156,20 +183,15 @@ private synchronized void parseTokenResponse(Response response) throw new ApiException( response.message(), response.code(), response.headers().toMultimap(), body); } - if (response.body() == null) { + if (response.body() == null || response.body().contentLength() == 0) { throw new JsonSyntaxException("body from token creation is null"); } - token = + KeyFlowTokenResponse keyFlowTokenResponse = gson.fromJson( new InputStreamReader(response.body().byteStream(), StandardCharsets.UTF_8), KeyFlowTokenResponse.class); - token.expiresIn = - JWT.decode(token.accessToken) - .getExpiresAt() - .toInstant() - .minusSeconds(tokenLeewayInSeconds) - .getEpochSecond(); + setToken(keyFlowTokenResponse); response.body().close(); } @@ -189,6 +211,16 @@ private Call requestToken(String grant, String assertionValue) throws IOExceptio return httpClient.newCall(request); } + protected void setToken(KeyFlowTokenResponse response) { + token = response; + token.expiresIn = + JWT.decode(response.accessToken) + .getExpiresAt() + .toInstant() + .minusSeconds(tokenLeewayInSeconds) + .getEpochSecond(); + } + private String generateSelfSignedJWT() throws InvalidKeySpecException, NoSuchAlgorithmException { RSAPrivateKey prvKey; diff --git a/core/src/main/java/cloud/stackit/sdk/core/KeyFlowInterceptor.java b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowInterceptor.java index ae67699..b9fdafd 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/KeyFlowInterceptor.java +++ b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowInterceptor.java @@ -2,6 +2,7 @@ import cloud.stackit.sdk.core.exception.ApiException; import java.io.IOException; +import java.security.spec.InvalidKeySpecException; import okhttp3.Interceptor; import okhttp3.Request; import okhttp3.Response; @@ -16,11 +17,12 @@ public KeyFlowInterceptor(KeyFlowAuthenticator authenticator) { @NotNull @Override public Response intercept(Chain chain) throws IOException { + Request originalRequest = chain.request(); String accessToken; try { accessToken = authenticator.getAccessToken(); - } catch (ApiException e) { + } catch (InvalidKeySpecException | ApiException e) { // try-catch required, because ApiException can not be thrown in the implementation // of Interceptor.intercept(Chain chain) throw new RuntimeException(e); diff --git a/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java b/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java index 645c8bd..09a4c88 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java +++ b/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java @@ -4,10 +4,10 @@ import cloud.stackit.sdk.core.KeyFlowInterceptor; import cloud.stackit.sdk.core.config.CoreConfiguration; import cloud.stackit.sdk.core.config.EnvironmentVariables; -import cloud.stackit.sdk.core.exception.ApiException; import cloud.stackit.sdk.core.exception.CredentialsInFileNotFoundException; import cloud.stackit.sdk.core.exception.PrivateKeyNotFoundException; import cloud.stackit.sdk.core.model.ServiceAccountKey; +import cloud.stackit.sdk.core.utils.Utils; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import java.io.File; @@ -16,13 +16,14 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; -import java.security.spec.InvalidKeySpecException; import java.util.Map; import javax.swing.filechooser.FileSystemView; import okhttp3.Interceptor; public class SetupAuth { - private final Interceptor authHandler; + private final EnvironmentVariables env; + private Interceptor authHandler; + private final CoreConfiguration cfg; private final String defaultCredentialsFilePath = FileSystemView.getFileSystemView().getHomeDirectory() + File.separator @@ -35,17 +36,10 @@ public class SetupAuth { * `SetupAuth().getAuthHandler()` as interceptor. This relies on the configuration methods via * ENVs or the credentials file in `$HOME/.stackit/credentials.json` * - * @throws IOException when a file can be found * @throws CredentialsInFileNotFoundException when no configuration is set or can be found - * @throws InvalidKeySpecException when the private key can not be parsed - * @throws ApiException when access token creation failed */ - public SetupAuth() - throws IOException, - InvalidKeySpecException, - CredentialsInFileNotFoundException, - ApiException { - this(new CoreConfiguration.Builder().build()); + public SetupAuth() throws CredentialsInFileNotFoundException { + this(new CoreConfiguration.Builder().build(), new EnvironmentVariables()); } /** @@ -56,23 +50,35 @@ public SetupAuth() * used * @throws IOException when a file can be found * @throws CredentialsInFileNotFoundException when no credentials are set or can be found - * @throws InvalidKeySpecException when the private key can not be parsed - * @throws ApiException when access token creation failed */ - public SetupAuth(CoreConfiguration cfg) - throws IOException, - CredentialsInFileNotFoundException, - InvalidKeySpecException, - ApiException { - if (cfg == null) { - cfg = new CoreConfiguration.Builder().build(); - } + public SetupAuth(CoreConfiguration cfg) throws IOException, CredentialsInFileNotFoundException { + this(cfg, new EnvironmentVariables()); + } + + /** + * Set up the KeyFlow Authentication and can be integrated in an OkHttp client, by adding + * `SetupAuth().getAuthHandler()` as interceptor. + * + * @param cfg Configuration which describes, which service account and token endpoint should be + * used + * @throws CredentialsInFileNotFoundException when no credentials are set or can be found + */ + protected SetupAuth(CoreConfiguration cfg, EnvironmentVariables environmentVariables) + throws CredentialsInFileNotFoundException { + + this.cfg = cfg != null ? cfg : new CoreConfiguration.Builder().build(); + this.env = environmentVariables != null ? environmentVariables : new EnvironmentVariables(); + } + public void init() throws IOException { ServiceAccountKey saKey = setupKeyFlow(cfg); authHandler = new KeyFlowInterceptor(new KeyFlowAuthenticator(cfg, saKey)); } public Interceptor getAuthHandler() { + if (authHandler == null) { + throw new RuntimeException("init() has to be called first"); + } return authHandler; } @@ -108,17 +114,16 @@ public Interceptor getAuthHandler() { * can be found * @throws IOException thrown when a file can not be found */ - private ServiceAccountKey setupKeyFlow(CoreConfiguration cfg) + protected ServiceAccountKey setupKeyFlow(CoreConfiguration cfg) throws CredentialsInFileNotFoundException, IOException { // Explicit config in code - if (cfg.getServiceAccountKey() != null && !cfg.getServiceAccountKey().trim().isEmpty()) { + if (Utils.isStringSet(cfg.getServiceAccountKey())) { ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(cfg.getServiceAccountKey()); loadPrivateKey(cfg, saKey); return saKey; } - if (cfg.getServiceAccountKeyPath() != null - && !cfg.getServiceAccountKeyPath().trim().isEmpty()) { + if (Utils.isStringSet(cfg.getServiceAccountKeyPath())) { String fileContent = new String( Files.readAllBytes(Paths.get(cfg.getServiceAccountKeyPath())), @@ -129,49 +134,41 @@ private ServiceAccountKey setupKeyFlow(CoreConfiguration cfg) } // Env config - if (EnvironmentVariables.STACKIT_SERVICE_ACCOUNT_KEY != null - && !EnvironmentVariables.STACKIT_SERVICE_ACCOUNT_KEY.trim().isEmpty()) { + if (Utils.isStringSet(env.getStackitServiceAccountKey())) { ServiceAccountKey saKey = - ServiceAccountKey.loadFromJson( - EnvironmentVariables.STACKIT_SERVICE_ACCOUNT_KEY.trim()); + ServiceAccountKey.loadFromJson(env.getStackitServiceAccountKey().trim()); loadPrivateKey(cfg, saKey); return saKey; } - if (EnvironmentVariables.STACKIT_SERVICE_ACCOUNT_KEY_PATH != null - && !EnvironmentVariables.STACKIT_SERVICE_ACCOUNT_KEY_PATH.trim().isEmpty()) { + if (Utils.isStringSet(env.getStackitServiceAccountKeyPath())) { String fileContent = new String( - Files.readAllBytes(Paths.get(cfg.getServiceAccountKeyPath())), + Files.readAllBytes(Paths.get(env.getStackitServiceAccountKeyPath())), StandardCharsets.UTF_8); ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(fileContent); loadPrivateKey(cfg, saKey); return saKey; } - if (EnvironmentVariables.STACKIT_CREDENTIALS_PATH != null - && !EnvironmentVariables.STACKIT_CREDENTIALS_PATH.trim().isEmpty()) { - String saKeyJson = - readValueFromCredentialsFile( - EnvironmentVariables.STACKIT_CREDENTIALS_PATH, - EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY, - EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY_PATH); - ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(saKeyJson); - loadPrivateKey(cfg, saKey); - return saKey; - } else { - String saKeyJson = - readValueFromCredentialsFile( - defaultCredentialsFilePath, - EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY, - EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY_PATH); - ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(saKeyJson); - loadPrivateKey(cfg, saKey); - return saKey; - } + // Read from credentialsFile + String credentialsFilePath = + Utils.isStringSet(env.getStackitCredentialsPath()) + ? env.getStackitCredentialsPath() + : defaultCredentialsFilePath; + + String saKeyJson = + readValueFromCredentialsFile( + credentialsFilePath, + EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY, + EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY_PATH); + + ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(saKeyJson); + loadPrivateKey(cfg, saKey); + return saKey; } - private void loadPrivateKey(CoreConfiguration cfg, ServiceAccountKey saKey) + protected void loadPrivateKey(CoreConfiguration cfg, ServiceAccountKey saKey) throws PrivateKeyNotFoundException { if (!saKey.getCredentials().isPrivateKeySet()) { try { @@ -216,18 +213,17 @@ private String getPrivateKey(CoreConfiguration cfg) throws CredentialsInFileNotFoundException, IOException { // Explicit code config // Set private key - if (cfg.getPrivateKey() != null && !cfg.getPrivateKey().trim().isEmpty()) { + if (Utils.isStringSet(cfg.getPrivateKey())) { return cfg.getPrivateKey(); } // Set private key path - if (cfg.getPrivateKeyPath() != null && !cfg.getPrivateKeyPath().trim().isEmpty()) { + if (Utils.isStringSet(cfg.getPrivateKeyPath())) { String privateKeyPath = cfg.getPrivateKeyPath(); return new String( Files.readAllBytes(Paths.get(privateKeyPath)), StandardCharsets.UTF_8); } // Set credentials file - if (cfg.getCredentialsFilePath() != null - && !cfg.getCredentialsFilePath().trim().isEmpty()) { + if (Utils.isStringSet(cfg.getCredentialsFilePath())) { return readValueFromCredentialsFile( cfg.getCredentialsFilePath(), EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY, @@ -235,27 +231,22 @@ private String getPrivateKey(CoreConfiguration cfg) } // ENVs config - if (EnvironmentVariables.STACKIT_PRIVATE_KEY != null - && !EnvironmentVariables.STACKIT_PRIVATE_KEY.trim().isEmpty()) { - return EnvironmentVariables.STACKIT_PRIVATE_KEY.trim(); + if (Utils.isStringSet(env.getStackitPrivateKey())) { + return env.getStackitPrivateKey().trim(); } - if (EnvironmentVariables.STACKIT_PRIVATE_KEY_PATH != null - && !EnvironmentVariables.STACKIT_PRIVATE_KEY_PATH.trim().isEmpty()) { + if (Utils.isStringSet(env.getStackitPrivateKeyPath())) { return new String( - Files.readAllBytes(Paths.get(EnvironmentVariables.STACKIT_PRIVATE_KEY_PATH)), + Files.readAllBytes(Paths.get(env.getStackitPrivateKeyPath())), StandardCharsets.UTF_8); } - if (EnvironmentVariables.STACKIT_CREDENTIALS_PATH != null - && !EnvironmentVariables.STACKIT_CREDENTIALS_PATH.trim().isEmpty()) { - return readValueFromCredentialsFile( - EnvironmentVariables.STACKIT_CREDENTIALS_PATH, - EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY, - EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY_PATH); - } - // Read from credentials file in defaultCredentialsFilePath + String credentialsFilePath = + Utils.isStringSet(env.getStackitCredentialsPath()) + ? env.getStackitCredentialsPath() + : defaultCredentialsFilePath; + return readValueFromCredentialsFile( - defaultCredentialsFilePath, + credentialsFilePath, EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY, EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY_PATH); } @@ -272,7 +263,7 @@ private String getPrivateKey(CoreConfiguration cfg) * @throws IOException throws if the provided path can not be found or the file within the * pathKey can not be found */ - private String readValueFromCredentialsFile(String path, String valueKey, String pathKey) + protected String readValueFromCredentialsFile(String path, String valueKey, String pathKey) throws IOException, CredentialsInFileNotFoundException { // Read credentials file String fileContent = @@ -286,7 +277,7 @@ private String readValueFromCredentialsFile(String path, String valueKey, String key = (String) map.get(valueKey); } catch (ClassCastException ignored) { } - if (key != null && !key.trim().isEmpty()) { + if (Utils.isStringSet(key)) { return key; } @@ -296,7 +287,7 @@ private String readValueFromCredentialsFile(String path, String valueKey, String keyPath = (String) map.get(pathKey); } catch (ClassCastException ignored) { } - if (keyPath != null && !keyPath.trim().isEmpty()) { + if (Utils.isStringSet(keyPath)) { return new String(Files.readAllBytes(Paths.get(keyPath))); } throw new CredentialsInFileNotFoundException( diff --git a/core/src/main/java/cloud/stackit/sdk/core/config/EnvironmentVariables.java b/core/src/main/java/cloud/stackit/sdk/core/config/EnvironmentVariables.java index c005ae5..42b920e 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/config/EnvironmentVariables.java +++ b/core/src/main/java/cloud/stackit/sdk/core/config/EnvironmentVariables.java @@ -9,38 +9,27 @@ public class EnvironmentVariables { public static final String ENV_STACKIT_TOKEN_BASEURL = "STACKIT_TOKEN_BASEURL"; public static final String ENV_STACKIT_CREDENTIALS_PATH = "STACKIT_CREDENTIALS_PATH"; - public static final String STACKIT_SERVICE_ACCOUNT_KEY_PATH = - System.getenv(ENV_STACKIT_SERVICE_ACCOUNT_KEY_PATH); - public static final String STACKIT_SERVICE_ACCOUNT_KEY = - System.getenv(ENV_STACKIT_SERVICE_ACCOUNT_KEY); - public static final String STACKIT_PRIVATE_KEY_PATH = - System.getenv(ENV_STACKIT_PRIVATE_KEY_PATH); - public static final String STACKIT_PRIVATE_KEY = System.getenv(ENV_STACKIT_PRIVATE_KEY); - public static final String STACKIT_TOKEN_BASEURL = System.getenv(ENV_STACKIT_TOKEN_BASEURL); - public static final String STACKIT_CREDENTIALS_PATH = - System.getenv(ENV_STACKIT_CREDENTIALS_PATH); + public String getStackitServiceAccountKeyPath() { + return System.getenv(ENV_STACKIT_SERVICE_ACCOUNT_KEY_PATH); + } + + public String getStackitServiceAccountKey() { + return System.getenv(ENV_STACKIT_SERVICE_ACCOUNT_KEY); + } + + public String getStackitPrivateKeyPath() { + return System.getenv(ENV_STACKIT_PRIVATE_KEY_PATH); + } + + public String getStackitPrivateKey() { + return System.getenv(ENV_STACKIT_PRIVATE_KEY); + } + + public String getStackitTokenBaseurl() { + return System.getenv(ENV_STACKIT_TOKEN_BASEURL); + } - @Override - public String toString() { - return "EnvironmentVariables{" - + "STACKIT_SERVICE_ACCOUNT_KEY_PATH='" - + STACKIT_SERVICE_ACCOUNT_KEY_PATH - + '\'' - + ", STACKIT_SERVICE_ACCOUNT_KEY='" - + STACKIT_SERVICE_ACCOUNT_KEY - + '\'' - + ", STACKIT_PRIVATE_KEY_PATH='" - + STACKIT_PRIVATE_KEY_PATH - + '\'' - + ", STACKIT_PRIVATE_KEY='" - + STACKIT_PRIVATE_KEY - + '\'' - + ", STACKIT_TOKEN_BASEURL='" - + STACKIT_TOKEN_BASEURL - + '\'' - + ", STACKIT_CREDENTIALS_PATH='" - + STACKIT_CREDENTIALS_PATH - + '\'' - + '}'; + public String getStackitCredentialsPath() { + return System.getenv(ENV_STACKIT_CREDENTIALS_PATH); } } diff --git a/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountCredentials.java b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountCredentials.java index 83dc2df..fca5510 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountCredentials.java +++ b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountCredentials.java @@ -1,11 +1,13 @@ package cloud.stackit.sdk.core.model; +import cloud.stackit.sdk.core.utils.Utils; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.interfaces.RSAPrivateKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Base64; +import java.util.Objects; public class ServiceAccountCredentials { private final String aud; @@ -44,7 +46,7 @@ public void setPrivateKey(String privateKey) { } public boolean isPrivateKeySet() { - return privateKey != null && !privateKey.trim().isEmpty(); + return Utils.isStringSet(privateKey); } public String getSub() { @@ -62,4 +64,21 @@ public RSAPrivateKey getPrivateKeyParsed() KeyFactory keyFactory = KeyFactory.getInstance("RSA"); return (RSAPrivateKey) keyFactory.generatePrivate(keySpec); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ServiceAccountCredentials that = (ServiceAccountCredentials) o; + return Objects.equals(aud, that.aud) + && Objects.equals(iss, that.iss) + && Objects.equals(kid, that.kid) + && Objects.equals(privateKey, that.privateKey) + && Objects.equals(sub, that.sub); + } + + @Override + public int hashCode() { + return Objects.hash(aud, iss, kid, privateKey, sub); + } } diff --git a/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java index bbc7ed1..a84adf6 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java +++ b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java @@ -3,6 +3,7 @@ import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; import java.util.Date; +import java.util.Objects; public class ServiceAccountKey { private final String id; @@ -84,4 +85,34 @@ public static ServiceAccountKey loadFromJson(String json) throws JsonSyntaxExcep private boolean isCredentialsSet() { return credentials != null; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ServiceAccountKey that = (ServiceAccountKey) o; + return active == that.active + && Objects.equals(id, that.id) + && Objects.equals(publicKey, that.publicKey) + && Objects.equals(created, that.created) + && Objects.equals(keyType, that.keyType) + && Objects.equals(keyOrigin, that.keyOrigin) + && Objects.equals(keyAlgorithm, that.keyAlgorithm) + && Objects.equals(validUntil, that.validUntil) + && Objects.equals(credentials, that.credentials); + } + + @Override + public int hashCode() { + return Objects.hash( + id, + publicKey, + created, + keyType, + keyOrigin, + keyAlgorithm, + active, + validUntil, + credentials); + } } diff --git a/core/src/main/java/cloud/stackit/sdk/core/utils/Utils.java b/core/src/main/java/cloud/stackit/sdk/core/utils/Utils.java new file mode 100644 index 0000000..5a88097 --- /dev/null +++ b/core/src/main/java/cloud/stackit/sdk/core/utils/Utils.java @@ -0,0 +1,7 @@ +package cloud.stackit.sdk.core.utils; + +public final class Utils { + public static boolean isStringSet(String input) { + return input != null && !input.trim().isEmpty(); + } +} diff --git a/core/src/test/java/cloud/stackit/sdk/core/KeyFlowAuthenticatorTest.java b/core/src/test/java/cloud/stackit/sdk/core/KeyFlowAuthenticatorTest.java new file mode 100644 index 0000000..0465171 --- /dev/null +++ b/core/src/test/java/cloud/stackit/sdk/core/KeyFlowAuthenticatorTest.java @@ -0,0 +1,267 @@ +package cloud.stackit.sdk.core; + +import static org.junit.jupiter.api.Assertions.*; + +import cloud.stackit.sdk.core.config.CoreConfiguration; +import cloud.stackit.sdk.core.exception.ApiException; +import cloud.stackit.sdk.core.model.ServiceAccountCredentials; +import cloud.stackit.sdk.core.model.ServiceAccountKey; +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPrivateKey; +import java.security.spec.InvalidKeySpecException; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import okhttp3.HttpUrl; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class KeyFlowAuthenticatorTest { + private static MockWebServer mockWebServer; + private ServiceAccountKey defaultSaKey; + private final String privateKey = + "-----BEGIN PRIVATE KEY-----\n" + + "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC0jVPq7ACbkwW6\n" + + "ojf6akoAlqkSLpAaTESOKEw6Hi2chr6gV4I1jtVLJM5K1e+vR+bKFBAzBVk9NCKS\n" + + "EiN+fTzEuz+z7sEhM5yBv4LUCrk3HoUT0nptFCLlJ40dmFlqmFSRtqSfX04kCs8N\n" + + "+HqTgCWEMVKd4Vq75jJAH2QYKLTa9nWPolxZK+e2twd3HQoVFP9fZQPNK0TNrn85\n" + + "beA1MYYT7U+oxAJEzPrFHcmJvvZW88rL7iZ0A7lqpXSVg/Uvu29nfW7lIZgc/FFI\n" + + "eWeHySKJ68YLzl4WV5rNRqzcJ2NKMn7+j1SYSJvsOrlAcptz5jyp/pwyVK3RpoQt\n" + + "mKsZKZJHAgMBAAECggEBAJSdH83mpDlqMvUEQX9lrbP+TvwgR4zd6i/5C4VrAbAt\n" + + "WQsx/IOJJhfMG+GNZtSoIleDXDIi3Ol72FjThVPAUhy85Bp/E4j4qoJB2LYgfYPZ\n" + + "I0DFpu/R+0cT3xvVIwSSjknCRI7KK8+O9g9Rz9NJT5gX4SEDNWQkfog5TnJ0TylL\n" + + "Ako6XIXFUar93AWuRKJgrsNvt/47ojb4hbTe8kUSMK2yXUJ80AvjHw0TiAFLjBxw\n" + + "YvlYtaoSbBB5Wp+3FuedKfJWbnMs6K/EwH1McDFc1xo9zS/ovmV2/YlRWDzRIJ5n\n" + + "ozTbHjnOmJ8ZF3NQh7kX0UmUbUi+qdL4yY1ON9SWmYECgYEA20YNmAJIrm6RrhA1\n" + + "2bCshnWqOw7PIkV3Pv3U5gPqZjaT9wSgnI2y/rauooTjiT/gNSRlUZZot+4hmKq5\n" + + "dxAlyFv8ibQYKRpwAJUMtTL8W66YMpfGkZ4WQ6hOsuC7ZcpGRyUXIPgqv/JOxR0D\n" + + "PpAGkAIU5bhauZRW8Yl54gy0f1kCgYEA0sr7lTlNy4XNxJli1L2tX7jzkF/2AZDb\n" + + "4ltj8aGYkhpOjNIHSn2lHQwbJiksTfVq+8XZ7hlwOk6DbT5Ev1qZ72i+lcxpsf8a\n" + + "/NNn7MUw9woAIYe+iKJfA9jagg73rT5HjhXXob1KAFui4Jp00MCk5JJ5MGV+jf2+\n" + + "mY9Q/TMdCp8CgYAfDIxgOfKQwJdgTmtRp/LGF2NDeZVbBPsdsFO1PliyoIfTMpSL\n" + + "loUCDFwuJyMRDDpzS/QM2X96i/214HbipSa0eFIKLbY+G8BAVNq3zcBuOwrSHyu+\n" + + "8uO0MODz816Vy06oRFhCEuH6zBTbVIBhG4PSYHkVDkXKgXbOPOlFWQc2AQKBgQC8\n" + + "yUyO9haFi52hURqhnAsVquiAymDiQCGeVelp9CdX2rW1Czm6blMdc8Uw5TknzP/2\n" + + "49jtlNzda4nrohQiKPuq3m2qbbvPzcEW5CO0e1sCNXOulAuCBaIBKQtx5gPOpfOh\n" + + "/k/0LDqFnYx/ifXLLG3BxKlDPfMdKj+0+hU337pH0wKBgHZ4A6fWlsWMXjr2blLt\n" + + "eHOkcU4xJ6Rkwnpn03IgWwbMV6UAXOLOfJ16JPNGQ9xxt4uEo4BMTBXr30l1LULj\n" + + "5vKz3Q54ZQY8DYKoQ0b66MZX/YeFvCKC3pr7YILBXt1gPA+/9PXSKPb9HlE8NSG6\n" + + "h/9afEtu5aUE/m+1vGBoH8z1\n" + + "-----END PRIVATE KEY-----\n"; + + ServiceAccountKey createDummyServiceAccount() { + ServiceAccountCredentials credentials = + new ServiceAccountCredentials("aud", "iss", "kid", privateKey, "sub"); + return new ServiceAccountKey( + "id", + "publicKey", + Date.from( // Workaround that ServiceAccountKey can be compared in tests + new Date().toInstant().truncatedTo(ChronoUnit.SECONDS)), + "keyType", + "keyOrigin", + "keyAlgo", + true, + Date.from( // Workaround that ServiceAccountKey can be compared in tests + new Date().toInstant().truncatedTo(ChronoUnit.SECONDS)), + credentials); + } + + KeyFlowAuthenticator.KeyFlowTokenResponse mockResponseBody(boolean expired) + throws NoSuchAlgorithmException, InvalidKeySpecException { + Date issuedAt = new Date(); + Date expiredAt = Date.from(new Date().toInstant().plusSeconds(60 * 10)); + if (expired) { + expiredAt = Date.from(new Date().toInstant().minusSeconds(60 * 10)); + } + + // Create mock response + RSAPrivateKey signingKey = defaultSaKey.getCredentials().getPrivateKeyParsed(); + Algorithm algorithm = Algorithm.RSA512(signingKey); + String accessTokenResponse = + JWT.create().withIssuedAt(issuedAt).withExpiresAt(expiredAt).sign(algorithm); + return new KeyFlowAuthenticator.KeyFlowTokenResponse( + accessTokenResponse, accessTokenResponse, 0, "scope", "USER_GENERATED"); + } + + @BeforeEach + void setUp() throws IOException { + mockWebServer = new MockWebServer(); + mockWebServer.start(); + defaultSaKey = createDummyServiceAccount(); + } + + @AfterEach + void tearDown() throws IOException { + mockWebServer.shutdown(); + } + + @Test + void getAccessToken_response200_noException() + throws NoSuchAlgorithmException, InvalidKeySpecException, IOException, ApiException { + + // Setup mockServer + KeyFlowAuthenticator.KeyFlowTokenResponse responseBody = mockResponseBody(false); + String responseBodyJson = new Gson().toJson(responseBody); + MockResponse mockedResponse = + new MockResponse().setBody(responseBodyJson).setResponseCode(200); + mockWebServer.enqueue(mockedResponse); + + // Config + HttpUrl url = mockWebServer.url("/token"); + CoreConfiguration cfg = + new CoreConfiguration.Builder() + .tokenCustomUrl(url.toString()) // Use mockWebServer + .build(); + + KeyFlowAuthenticator keyFlowAuthenticator = new KeyFlowAuthenticator(cfg, defaultSaKey); + + assertDoesNotThrow(keyFlowAuthenticator::getAccessToken); + assertEquals(responseBody.getAccessToken(), keyFlowAuthenticator.getAccessToken()); + } + + @Test + void getAccessToken_expiredToken_noException() + throws NoSuchAlgorithmException, InvalidKeySpecException, IOException, ApiException { + // Setup expiredToken and newToken + KeyFlowAuthenticator.KeyFlowTokenResponse expiredKey = mockResponseBody(true); + + KeyFlowAuthenticator.KeyFlowTokenResponse newToken = mockResponseBody(false); + + // Setup mockServer + String responseBodyJson = new Gson().toJson(newToken); + MockResponse mockedResponse = + new MockResponse().setBody(responseBodyJson).setResponseCode(200); + mockWebServer.enqueue(mockedResponse); + + // Config + HttpUrl url = mockWebServer.url("/token"); + CoreConfiguration cfg = + new CoreConfiguration.Builder() + .tokenCustomUrl(url.toString()) // Use mockWebServer + .build(); + + KeyFlowAuthenticator keyFlowAuthenticator = new KeyFlowAuthenticator(cfg, defaultSaKey); + keyFlowAuthenticator.setToken(expiredKey); + + assertEquals(newToken.getAccessToken(), keyFlowAuthenticator.getAccessToken()); + } + + @Test + void createAccessToken_response200WithEmptyBody_throwsException() { + // Setup mockServer + MockResponse mockedResponse = new MockResponse().setResponseCode(200); + mockWebServer.enqueue(mockedResponse); + HttpUrl url = mockWebServer.url("/token"); + + // Config + CoreConfiguration cfg = + new CoreConfiguration.Builder() + .tokenCustomUrl(url.toString()) // Use mockWebServer + .build(); + + // Init keyFlowAuthenticator + KeyFlowAuthenticator keyFlowAuthenticator = + new KeyFlowAuthenticator(cfg, createDummyServiceAccount()); + + assertThrows(JsonSyntaxException.class, keyFlowAuthenticator::createAccessToken); + } + + @Test + void createAccessToken_response400_throwsApiException() { + // Setup mockServer + MockResponse mockedResponse = new MockResponse().setResponseCode(400); + mockWebServer.enqueue(mockedResponse); + HttpUrl url = mockWebServer.url("/token"); + + // Config + CoreConfiguration cfg = + new CoreConfiguration.Builder() + .tokenCustomUrl(url.toString()) // Use mockWebServer + .build(); + + // Init keyFlowAuthenticator + KeyFlowAuthenticator keyFlowAuthenticator = + new KeyFlowAuthenticator(cfg, createDummyServiceAccount()); + + assertThrows(ApiException.class, keyFlowAuthenticator::createAccessToken); + } + + @Test + void createAccessToken_response200WithValidResponse_noException() + throws NoSuchAlgorithmException, InvalidKeySpecException { + // Setup mockServer + KeyFlowAuthenticator.KeyFlowTokenResponse responseBody = mockResponseBody(false); + String responseBodyJson = new Gson().toJson(responseBody); + MockResponse mockedResponse = + new MockResponse().setBody(responseBodyJson).setResponseCode(200); + mockWebServer.enqueue(mockedResponse); + + // Config + HttpUrl url = mockWebServer.url("/token"); + CoreConfiguration cfg = + new CoreConfiguration.Builder() + .tokenCustomUrl(url.toString()) // Use mockWebServer + .build(); + + // Init keyFlowAuthenticator + KeyFlowAuthenticator keyFlowAuthenticator = new KeyFlowAuthenticator(cfg, defaultSaKey); + + assertDoesNotThrow(keyFlowAuthenticator::createAccessToken); + } + + @Test + void createAccessTokenWithRefreshToken_response200WithValidResponse_noException() + throws NoSuchAlgorithmException, InvalidKeySpecException { + // Setup mockServer + KeyFlowAuthenticator.KeyFlowTokenResponse mockedBody = mockResponseBody(false); + String mockedBodyJson = new Gson().toJson(mockedBody); + MockResponse mockedResponse = + new MockResponse().setBody(mockedBodyJson).setResponseCode(200); + mockWebServer.enqueue(mockedResponse); + + // Config + HttpUrl url = mockWebServer.url("/token"); + CoreConfiguration cfg = + new CoreConfiguration.Builder() + .tokenCustomUrl(url.toString()) // Use mockWebServer + .build(); + + // Prepare keyFlowAuthenticator + KeyFlowAuthenticator keyFlowAuthenticator = new KeyFlowAuthenticator(cfg, defaultSaKey); + keyFlowAuthenticator.setToken(mockedBody); + + assertDoesNotThrow(keyFlowAuthenticator::createAccessTokenWithRefreshToken); + } + + @Test + void createAccessTokenWithRefreshToken_response200WithEmptyBody_throwsException() + throws NoSuchAlgorithmException, InvalidKeySpecException { + // Setup mockServer + KeyFlowAuthenticator.KeyFlowTokenResponse mockResponse = mockResponseBody(false); + MockResponse mockedResponse = new MockResponse().setResponseCode(200); + mockWebServer.enqueue(mockedResponse); + HttpUrl url = mockWebServer.url("/token"); + + // Config + CoreConfiguration cfg = + new CoreConfiguration.Builder() + .tokenCustomUrl(url.toString()) // Use mockWebServer + .build(); + + // Prepare keyFlowAuthenticator + KeyFlowAuthenticator keyFlowAuthenticator = + new KeyFlowAuthenticator(cfg, createDummyServiceAccount()); + keyFlowAuthenticator.setToken(mockResponse); + + // Refresh token + assertThrows( + JsonSyntaxException.class, keyFlowAuthenticator::createAccessTokenWithRefreshToken); + } +} diff --git a/core/src/test/java/cloud/stackit/sdk/core/KeyFlowInterceptorTest.java b/core/src/test/java/cloud/stackit/sdk/core/KeyFlowInterceptorTest.java new file mode 100644 index 0000000..4927ad7 --- /dev/null +++ b/core/src/test/java/cloud/stackit/sdk/core/KeyFlowInterceptorTest.java @@ -0,0 +1,62 @@ +package cloud.stackit.sdk.core; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +import cloud.stackit.sdk.core.exception.ApiException; +import java.io.IOException; +import java.security.spec.InvalidKeySpecException; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class KeyFlowInterceptorTest { + + @Mock private KeyFlowAuthenticator authenticator; + // private KeyFlowInterceptor interceptor; + private MockWebServer mockWebServer; + private OkHttpClient client; + + @BeforeEach + void setUp() throws IOException { + mockWebServer = new MockWebServer(); + mockWebServer.start(); + + client = + new OkHttpClient.Builder() + .addInterceptor(new KeyFlowInterceptor(authenticator)) + .build(); + } + + @AfterEach + void teardown() throws IOException { + mockWebServer.shutdown(); + } + + @Test + void intercept_addsAuthHeader() + throws IOException, InvalidKeySpecException, ApiException, InterruptedException { + final String accessToken = "my-access-token"; + when(authenticator.getAccessToken()).thenReturn(accessToken); + + mockWebServer.enqueue(new MockResponse().setResponseCode(200)); + + // Make request + Request request = new Request.Builder().url(mockWebServer.url("/test")).build(); + client.newCall(request).execute(); + + RecordedRequest recordedRequest = mockWebServer.takeRequest(); + + String expectedAuthHeader = "Bearer " + accessToken; + assertEquals(expectedAuthHeader, recordedRequest.getHeader("Authorization")); + } +} diff --git a/core/src/test/java/cloud/stackit/sdk/core/auth/SetupAuthTest.java b/core/src/test/java/cloud/stackit/sdk/core/auth/SetupAuthTest.java new file mode 100644 index 0000000..6e1d0ec --- /dev/null +++ b/core/src/test/java/cloud/stackit/sdk/core/auth/SetupAuthTest.java @@ -0,0 +1,481 @@ +package cloud.stackit.sdk.core.auth; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +import cloud.stackit.sdk.core.config.CoreConfiguration; +import cloud.stackit.sdk.core.config.EnvironmentVariables; +import cloud.stackit.sdk.core.exception.ApiException; +import cloud.stackit.sdk.core.exception.PrivateKeyNotFoundException; +import cloud.stackit.sdk.core.model.ServiceAccountCredentials; +import cloud.stackit.sdk.core.model.ServiceAccountKey; +import com.google.gson.Gson; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.spec.InvalidKeySpecException; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import javax.swing.filechooser.FileSystemView; +import okhttp3.Interceptor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class SetupAuthTest { + @Mock private EnvironmentVariables envs; + private final String invalidCredentialsFilePath = + FileSystemView.getFileSystemView().getHomeDirectory() + + File.separator + + "invalid" + + File.separator + + "credentials" + + File.separator + + "file.json"; + + ServiceAccountKey createDummyServiceAccount(String privateKey) { + ServiceAccountCredentials credentials = + new ServiceAccountCredentials("aud", "iss", "kid", privateKey, "sub"); + return new ServiceAccountKey( + "id", + "publicKey", + Date.from( // Workaround that ServiceAccountKey can be compared in tests + new Date().toInstant().truncatedTo(ChronoUnit.SECONDS)), + "keyType", + "keyOrigin", + "keyAlgo", + true, + Date.from( // Workaround that ServiceAccountKey can be compared in tests + new Date().toInstant().truncatedTo(ChronoUnit.SECONDS)), + credentials); + } + + Path createJsonFile(Map content) throws IOException { + String contentJson = new Gson().toJson(content); + Path file = Files.createTempFile("credentials", ".json"); + file.toFile().deleteOnExit(); + + Files.write(file, contentJson.getBytes(StandardCharsets.UTF_8)); + return file; + } + + @Test + void getAccessToken_withoutRunningInit_throwsException() throws IOException { + SetupAuth setupAuth = new SetupAuth(); + assertThrows(RuntimeException.class, setupAuth::getAuthHandler); + } + + @Test + void getAccessToken_withRunningInit_returnsInterceptor() throws IOException { + ServiceAccountKey saKey = createDummyServiceAccount("privateKey"); + String initSaKeyJson = new Gson().toJson(saKey); + + CoreConfiguration config = + new CoreConfiguration.Builder().serviceAccountKey(initSaKeyJson).build(); + + SetupAuth setupAuth = new SetupAuth(config); + setupAuth.init(); + assertInstanceOf(Interceptor.class, setupAuth.getAuthHandler()); + } + + @Test + void setupKeyFlow_readServiceAccountFromPath() + throws IOException, InvalidKeySpecException, ApiException { + // Create service account key file + ServiceAccountKey initSaKey = createDummyServiceAccount("privateKey"); + String initSaKeyJson = new Gson().toJson(initSaKey); + Path saKeyPath = Files.createTempFile("serviceAccountKey", ".json"); + saKeyPath.toFile().deleteOnExit(); + Files.write(saKeyPath, initSaKeyJson.getBytes(StandardCharsets.UTF_8)); + + // Create config and read setup auth with the previous created saKey + CoreConfiguration cfg = + new CoreConfiguration.Builder() + .serviceAccountKeyPath(saKeyPath.toAbsolutePath().toString()) + .build(); + ServiceAccountKey parsedSaKey = new SetupAuth().setupKeyFlow(cfg); + + assertEquals(initSaKey, parsedSaKey); + } + + @Test + void setupKeyFlow_readServiceAccountFromConfig() + throws IOException, InvalidKeySpecException, ApiException { + // Create service account key + ServiceAccountKey initSaKey = createDummyServiceAccount("privateKey"); + String initSaKeyJson = new Gson().toJson(initSaKey); + + // Create config and read setup auth with the previous created saKey + CoreConfiguration cfg = + new CoreConfiguration.Builder().serviceAccountKey(initSaKeyJson).build(); + ServiceAccountKey parsedSaKey = new SetupAuth().setupKeyFlow(cfg); + + assertEquals(initSaKey, parsedSaKey); + } + + @Test + void setupKeyFlow_readServiceAccountFromKeyEnv() throws IOException { + // Create service account key + ServiceAccountKey initSaKey = createDummyServiceAccount("privateKey"); + String initSaKeyJson = new Gson().toJson(initSaKey); + + // Mock env STACKIT_SERVICE_ACCOUNT_KEY + when(envs.getStackitServiceAccountKey()).thenReturn(initSaKeyJson); + + // Create config and read setup auth with the previous created saKey + CoreConfiguration cfg = new CoreConfiguration.Builder().build(); + ServiceAccountKey parsedSaKey = new SetupAuth(cfg, envs).setupKeyFlow(cfg); + + assertEquals(initSaKey, parsedSaKey); + } + + @Test + void setupKeyFlow_readServiceAccountFromKeyPathEnv() throws IOException { + // Create service account key + ServiceAccountKey initSaKey = createDummyServiceAccount("privateKey"); + String keyPathContent = new Gson().toJson(initSaKey); + + // Create dummy keyPathFile + Path keyPathFile = Files.createTempFile("serviceAccountKey", ".json"); + keyPathFile.toFile().deleteOnExit(); + Files.write(keyPathFile, keyPathContent.getBytes(StandardCharsets.UTF_8)); + + // Mock env STACKIT_SERVICE_ACCOUNT_KEY_PATH + when(envs.getStackitServiceAccountKeyPath()) + .thenReturn(keyPathFile.toAbsolutePath().toString()); + + // Create config and read setup auth with the previous created saKey + CoreConfiguration cfg = new CoreConfiguration.Builder().build(); + ServiceAccountKey parsedSaKey = new SetupAuth(cfg, envs).setupKeyFlow(cfg); + + assertEquals(initSaKey, parsedSaKey); + } + + @Test + void setupKeyFlow_readServiceAccountFromPathWithoutPrivateKey_throwsException() + throws IOException, InvalidKeySpecException, ApiException { + // Create service account key file + ServiceAccountKey initSaKey = createDummyServiceAccount(null); + String initSaKeyJson = new Gson().toJson(initSaKey); + Path saKeyPath = Files.createTempFile("serviceAccountKey", ".json"); + saKeyPath.toFile().deleteOnExit(); + Files.write(saKeyPath, initSaKeyJson.getBytes(StandardCharsets.UTF_8)); + + // Create config and read setup auth with the previous created saKey + CoreConfiguration cfg = + new CoreConfiguration.Builder() + .serviceAccountKeyPath(saKeyPath.toAbsolutePath().toString()) + .credentialsFilePath( // make sure that the defaultCredentialsFile is not + // used + invalidCredentialsFilePath) + .build(); + SetupAuth auth = new SetupAuth(); + + assertThrows(PrivateKeyNotFoundException.class, () -> auth.setupKeyFlow(cfg)); + } + + @Test + void setupKeyFlow_readServiceAccountFromConfigWithoutPrivateKey_throwsException() + throws IOException, InvalidKeySpecException, ApiException { + // Create service account key + ServiceAccountKey initSaKey = createDummyServiceAccount(null); + String initSaKeyJson = new Gson().toJson(initSaKey); + + // Create config and read setup auth with the previous created saKey + CoreConfiguration cfg = + new CoreConfiguration.Builder() + .serviceAccountKey(initSaKeyJson) + .credentialsFilePath( // make sure that the defaultCredentialsFile is not + // used + invalidCredentialsFilePath) + .build(); + SetupAuth auth = new SetupAuth(); + + assertThrows(PrivateKeyNotFoundException.class, () -> auth.setupKeyFlow(cfg)); + } + + @Test + void loadPrivateKey_setPrivateKeyFromConfig() + throws IOException, InvalidKeySpecException, ApiException { + final String prvKey = "prvKey"; + ServiceAccountKey saKey = createDummyServiceAccount(null); + SetupAuth setupAuth = new SetupAuth(); + + CoreConfiguration cfg = new CoreConfiguration.Builder().privateKey(prvKey).build(); + + assertNull(saKey.getCredentials().getPrivateKey()); + assertDoesNotThrow(() -> setupAuth.loadPrivateKey(cfg, saKey)); + assertEquals(prvKey, saKey.getCredentials().getPrivateKey()); + } + + @Test + void loadPrivateKey_doesNotOverwriteExistingPrivateKey() + throws IOException, InvalidKeySpecException, ApiException { + final String initialPrivateKey = "prvKey"; + final String cfgPrivateKey = "prvKey-updated"; + + // Create Service Account + ServiceAccountKey saKey = createDummyServiceAccount(initialPrivateKey); + SetupAuth setupAuth = new SetupAuth(); + CoreConfiguration cfg = new CoreConfiguration.Builder().privateKey(cfgPrivateKey).build(); + + assertEquals(initialPrivateKey, saKey.getCredentials().getPrivateKey()); + assertDoesNotThrow(() -> setupAuth.loadPrivateKey(cfg, saKey)); + assertEquals(initialPrivateKey, saKey.getCredentials().getPrivateKey()); + } + + @Test + void loadPrivateKey_setPrivateKeyPath() + throws IOException, InvalidKeySpecException, ApiException { + Path tempPrvKeyFile = Files.createTempFile("privateKey", ".pem"); + tempPrvKeyFile.toFile().deleteOnExit(); + + final String privateKeyContent = ""; + Files.write(tempPrvKeyFile, privateKeyContent.getBytes(StandardCharsets.UTF_8)); + + // Create Service Account + ServiceAccountKey saKey = createDummyServiceAccount(null); + SetupAuth setupAuth = new SetupAuth(); + CoreConfiguration cfg = + new CoreConfiguration.Builder() + .privateKeyPath(tempPrvKeyFile.toAbsolutePath().toString()) + .build(); + + assertNull(saKey.getCredentials().getPrivateKey()); + assertDoesNotThrow(() -> setupAuth.loadPrivateKey(cfg, saKey)); + assertEquals(privateKeyContent, saKey.getCredentials().getPrivateKey()); + } + + @Test + void loadPrivateKey_setPrivateKeyPathViaCredentialsFile() + throws IOException, InvalidKeySpecException, ApiException { + // Create privateKeyFile + Path tempPrvKeyFile = Files.createTempFile("privateKey", ".pem"); + tempPrvKeyFile.toFile().deleteOnExit(); + + // Write private key file + final String privateKeyContent = ""; + Files.write(tempPrvKeyFile, privateKeyContent.getBytes(StandardCharsets.UTF_8)); + + // Create credentialsFile + Path tempCredentialsFile = Files.createTempFile("credentialsFile", ".json"); + tempCredentialsFile.toFile().deleteOnExit(); + + Map credFileContent = new HashMap<>(); + credFileContent.put( + EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY_PATH, + tempPrvKeyFile.toAbsolutePath().toString()); + String credFileContentJson = new Gson().toJson(credFileContent); + + // Write credentials file + Files.write(tempCredentialsFile, credFileContentJson.getBytes(StandardCharsets.UTF_8)); + + // Create ServiceAccount + ServiceAccountKey saKey = createDummyServiceAccount(null); + SetupAuth setupAuth = new SetupAuth(); + CoreConfiguration cfg = + new CoreConfiguration.Builder() + .credentialsFilePath(tempCredentialsFile.toAbsolutePath().toString()) + .build(); + + assertNull(saKey.getCredentials().getPrivateKey()); + assertDoesNotThrow(() -> setupAuth.loadPrivateKey(cfg, saKey)); + assertEquals(privateKeyContent, saKey.getCredentials().getPrivateKey()); + } + + @Test + void loadPrivateKey_setPrivateKeyViaCredentialsFile() + throws IOException, InvalidKeySpecException, ApiException { + final String privateKeyContent = ""; + + // Create credentialsFile + Path tempCredentialsFile = Files.createTempFile("credentialsFile", ".json"); + tempCredentialsFile.toFile().deleteOnExit(); + + // Create dummy credentialsFile + Map credFileContent = new HashMap<>(); + credFileContent.put(EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY, privateKeyContent); + String credFileContentJson = new Gson().toJson(credFileContent); + + Files.write(tempCredentialsFile, credFileContentJson.getBytes(StandardCharsets.UTF_8)); + + // Create dummy service account and config + ServiceAccountKey saKey = createDummyServiceAccount(null); + SetupAuth setupAuth = new SetupAuth(); + + CoreConfiguration cfg = + new CoreConfiguration.Builder() + .credentialsFilePath(tempCredentialsFile.toAbsolutePath().toString()) + .build(); + + assertNull(saKey.getCredentials().getPrivateKey()); + assertDoesNotThrow(() -> setupAuth.loadPrivateKey(cfg, saKey)); + assertEquals(privateKeyContent, saKey.getCredentials().getPrivateKey()); + } + + @Test + void loadPrivateKey_setPrivateKeyViaEnv() throws IOException { + final String prvKey = "prvKey"; + ServiceAccountKey saKey = createDummyServiceAccount(null); + when(envs.getStackitPrivateKey()).thenReturn(prvKey); + + CoreConfiguration cfg = new CoreConfiguration.Builder().build(); + SetupAuth setupAuth = new SetupAuth(cfg, envs); + + assertNull(saKey.getCredentials().getPrivateKey()); + assertDoesNotThrow(() -> setupAuth.loadPrivateKey(cfg, saKey)); + assertEquals(prvKey, saKey.getCredentials().getPrivateKey()); + } + + @Test + void loadPrivateKey_setPrivateKeyPathViaEnv() throws IOException { + final String prvKey = "prvKey"; + ServiceAccountKey saKey = createDummyServiceAccount(null); + Path tempPrvKeyFile = Files.createTempFile("privateKey", ".pem"); + tempPrvKeyFile.toFile().deleteOnExit(); + Files.write(tempPrvKeyFile, prvKey.getBytes(StandardCharsets.UTF_8)); + + when(envs.getStackitPrivateKeyPath()) + .thenReturn(tempPrvKeyFile.toAbsolutePath().toString()); + + CoreConfiguration cfg = new CoreConfiguration.Builder().build(); + SetupAuth setupAuth = new SetupAuth(cfg, envs); + + assertNull(saKey.getCredentials().getPrivateKey()); + assertDoesNotThrow(() -> setupAuth.loadPrivateKey(cfg, saKey)); + assertEquals(prvKey, saKey.getCredentials().getPrivateKey()); + } + + @Test + void loadPrivateKey_setPrivateKeyViaCredentialsFileInEnv() + throws IOException, InvalidKeySpecException, ApiException { + final String privateKeyContent = ""; + + // Create credentialsFile + Path tempCredentialsFile = Files.createTempFile("credentialsFile", ".json"); + tempCredentialsFile.toFile().deleteOnExit(); + + // Create dummy credentialsFile + Map credFileContent = new HashMap<>(); + credFileContent.put(EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY, privateKeyContent); + String credFileContentJson = new Gson().toJson(credFileContent); + + Files.write(tempCredentialsFile, credFileContentJson.getBytes(StandardCharsets.UTF_8)); + + // Create dummy service account and config + ServiceAccountKey saKey = createDummyServiceAccount(null); + CoreConfiguration cfg = new CoreConfiguration.Builder().build(); + SetupAuth setupAuth = new SetupAuth(cfg, envs); + when(envs.getStackitCredentialsPath()) + .thenReturn(tempCredentialsFile.toAbsolutePath().toString()); + + assertNull(saKey.getCredentials().getPrivateKey()); + assertDoesNotThrow(() -> setupAuth.loadPrivateKey(cfg, saKey)); + assertEquals(privateKeyContent, saKey.getCredentials().getPrivateKey()); + } + + @Test + void loadPrivateKey_invalidPrivateKeyPath_throwsException() + throws IOException, InvalidKeySpecException, ApiException { + + String invalidPath = + FileSystemView.getFileSystemView().getHomeDirectory() + + File.separator + + "invalid" + + File.separator + + "privateKey" + + File.separator + + "path.pem"; + + ServiceAccountKey saKey = createDummyServiceAccount(null); + SetupAuth setupAuth = new SetupAuth(); + + CoreConfiguration cfg = new CoreConfiguration.Builder().privateKeyPath(invalidPath).build(); + + assertNull(saKey.getCredentials().getPrivateKey()); + assertThrows(PrivateKeyNotFoundException.class, () -> setupAuth.loadPrivateKey(cfg, saKey)); + } + + @Test + void readValueFromCredentialsFile_keyAndKeyPathSet_returnsKeyValue() + throws IOException, InvalidKeySpecException, ApiException { + String keyContent = "key"; + String keyPathContent = "keyPath"; + + // Create dummy keyPathFile + Path keyPathFile = Files.createTempFile("serviceAccountKey", ".json"); + keyPathFile.toFile().deleteOnExit(); + Files.write(keyPathFile, keyPathContent.getBytes(StandardCharsets.UTF_8)); + + // Create dummy credentialsFile + Map credentialsFileContent = new HashMap<>(); + credentialsFileContent.put( + EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY, keyContent); + credentialsFileContent.put( + EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY_PATH, + keyPathFile.toAbsolutePath().toString()); + Path credentialsFile = createJsonFile(credentialsFileContent); + + String result = + new SetupAuth() + .readValueFromCredentialsFile( + credentialsFile.toAbsolutePath().toString(), + EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY, + EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY_PATH); + + assertEquals(keyContent, result); + } + + @Test + void readValueFromCredentialsFile_keySet_returnsKeyValue() + throws IOException, InvalidKeySpecException, ApiException { + String keyContent = "key"; + + // Create dummy credentialsFile + Map credentialsFileContent = new HashMap<>(); + credentialsFileContent.put( + EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY, keyContent); + Path credentialsFile = createJsonFile(credentialsFileContent); + + String result = + new SetupAuth() + .readValueFromCredentialsFile( + credentialsFile.toAbsolutePath().toString(), + EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY, + EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY_PATH); + + assertEquals(keyContent, result); + } + + @Test + void readValueFromCredentialsFile_KeyPathSet_returnsKeyValue() + throws IOException, InvalidKeySpecException, ApiException { + // Create dummy keyPathFile + String keyPathContent = "keyPath"; + Path keyPathFile = Files.createTempFile("serviceAccountKey", ".json"); + keyPathFile.toFile().deleteOnExit(); + Files.write(keyPathFile, keyPathContent.getBytes(StandardCharsets.UTF_8)); + + // Create dummy credentialsFile + Map credentialsFileContent = new HashMap<>(); + credentialsFileContent.put( + EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY_PATH, + keyPathFile.toAbsolutePath().toString()); + Path credentialsFile = createJsonFile(credentialsFileContent); + + String result = + new SetupAuth() + .readValueFromCredentialsFile( + credentialsFile.toAbsolutePath().toString(), + EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY, + EnvironmentVariables.ENV_STACKIT_SERVICE_ACCOUNT_KEY_PATH); + + assertEquals(keyPathContent, result); + } +} diff --git a/core/src/test/java/cloud/stackit/sdk/core/utils/UtilsTest.java b/core/src/test/java/cloud/stackit/sdk/core/utils/UtilsTest.java new file mode 100644 index 0000000..3180081 --- /dev/null +++ b/core/src/test/java/cloud/stackit/sdk/core/utils/UtilsTest.java @@ -0,0 +1,43 @@ +package cloud.stackit.sdk.core.utils; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class UtilsTest { + + @Test + void isStringSet_null_returnsFalse() { + assertFalse(Utils.isStringSet(null)); + } + + @Test + void isStringSet_nullString_returnsFalse() { + String nullString = null; + assertFalse(Utils.isStringSet(nullString)); + } + + @Test + void isStringSet_emptyString_returnsFalse() { + String nullString = ""; + assertFalse(Utils.isStringSet(nullString)); + } + + @Test + void isStringSet_stringWithWhitespaces_returnsFalse() { + String nullString = " "; + assertFalse(Utils.isStringSet(nullString)); + } + + @Test + void isStringSet_stringWithText_returnsTrue() { + String nullString = "text"; + assertTrue(Utils.isStringSet(nullString)); + } + + @Test + void isStringSet_stringWithTextAndWhitespaces_returnsTrue() { + String nullString = " text "; + assertTrue(Utils.isStringSet(nullString)); + } +} diff --git a/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/ApiClient.java b/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/ApiClient.java index 8fd8817..ecf69c8 100644 --- a/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/ApiClient.java +++ b/services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/ApiClient.java @@ -135,6 +135,7 @@ public ApiClient(CoreConfiguration config) } SetupAuth auth; auth = new SetupAuth(config); + auth.init(); List interceptors = new LinkedList<>(); interceptors.add(auth.getAuthHandler()); initHttpClient(interceptors); From 7f43e71ba998a91492d95c6db744cfb526852a37 Mon Sep 17 00:00:00 2001 From: Ruben Hoenle Date: Tue, 5 Aug 2025 18:07:45 +0200 Subject: [PATCH 10/12] feat(gradle): add execution task for examples --- build.gradle | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/build.gradle b/build.gradle index 70a21c3..4097515 100644 --- a/build.gradle +++ b/build.gradle @@ -118,6 +118,14 @@ subprojects { } } } + + // only apply to example sub-projects + if (project.path.startsWith(':examples:')) { + task execute(type:JavaExec) { + main = System.getProperty('mainClass') + classpath = sourceSets.main.runtimeClasspath + } + } } tasks.withType(Test).configureEach { From fbd3fc76c1bf081f0b173329367dc2c300844a5b Mon Sep 17 00:00:00 2001 From: Marcel Jacek Date: Thu, 7 Aug 2025 16:43:12 +0200 Subject: [PATCH 11/12] feedback review --- .../cloud/stackit/sdk/core/KeyFlowAuthenticator.java | 10 +++++++--- .../java/cloud/stackit/sdk/core/auth/SetupAuth.java | 9 ++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java index 21170ac..a6d3e55 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java +++ b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java @@ -24,12 +24,16 @@ import java.util.concurrent.TimeUnit; import okhttp3.*; + /** KeyFlowAuthenticator handles the Key Flow Authentication based on the Service Account Key. */ public class KeyFlowAuthenticator { private final String REFRESH_TOKEN = "refresh_token"; private final String ASSERTION = "assertion"; private final String DEFAULT_TOKEN_ENDPOINT = "https://service-account.api.stackit.cloud/token"; private final long DEFAULT_TOKEN_LEEWAY = 60; + private final int CONNECT_TIMEOUT = 10; + private final int WRITE_TIMEOUT = 10; + private final int READ_TIMEOUT = 10; private final OkHttpClient httpClient; private final ServiceAccountKey saKey; @@ -94,9 +98,9 @@ public KeyFlowAuthenticator( this.gson = new Gson(); this.httpClient = new OkHttpClient.Builder() - .connectTimeout(10, TimeUnit.SECONDS) - .writeTimeout(10, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) + .connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS) + .writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS) + .readTimeout(READ_TIMEOUT, TimeUnit.SECONDS) .build(); if (environmentVariables == null) { diff --git a/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java b/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java index 09a4c88..c23eaea 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java +++ b/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java @@ -212,17 +212,17 @@ protected void loadPrivateKey(CoreConfiguration cfg, ServiceAccountKey saKey) private String getPrivateKey(CoreConfiguration cfg) throws CredentialsInFileNotFoundException, IOException { // Explicit code config - // Set private key + // Get private key if (Utils.isStringSet(cfg.getPrivateKey())) { return cfg.getPrivateKey(); } - // Set private key path + // Get private key path if (Utils.isStringSet(cfg.getPrivateKeyPath())) { String privateKeyPath = cfg.getPrivateKeyPath(); return new String( Files.readAllBytes(Paths.get(privateKeyPath)), StandardCharsets.UTF_8); } - // Set credentials file + // Get credentials file if (Utils.isStringSet(cfg.getCredentialsFilePath())) { return readValueFromCredentialsFile( cfg.getCredentialsFilePath(), @@ -231,15 +231,18 @@ private String getPrivateKey(CoreConfiguration cfg) } // ENVs config + // Get private key if (Utils.isStringSet(env.getStackitPrivateKey())) { return env.getStackitPrivateKey().trim(); } + // Get private key path if (Utils.isStringSet(env.getStackitPrivateKeyPath())) { return new String( Files.readAllBytes(Paths.get(env.getStackitPrivateKeyPath())), StandardCharsets.UTF_8); } + // Get credentialsFilePath String credentialsFilePath = Utils.isStringSet(env.getStackitCredentialsPath()) ? env.getStackitCredentialsPath() From 1f7b6ba1aefcf848c25bac5607f885e98a5b1f3f Mon Sep 17 00:00:00 2001 From: Marcel Jacek Date: Thu, 7 Aug 2025 17:28:00 +0200 Subject: [PATCH 12/12] run formatter --- .../main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java index a6d3e55..b0e664a 100644 --- a/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java +++ b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java @@ -24,7 +24,6 @@ import java.util.concurrent.TimeUnit; import okhttp3.*; - /** KeyFlowAuthenticator handles the Key Flow Authentication based on the Service Account Key. */ public class KeyFlowAuthenticator { private final String REFRESH_TOKEN = "refresh_token";