diff --git a/build.gradle b/build.gradle index 2ebfd32..4097515 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' } @@ -118,9 +118,17 @@ 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) { + tasks.withType(Test).configureEach { // Enable JUnit 5 (Gradle 4.6+). useJUnitPlatform() @@ -138,5 +146,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' } } diff --git a/core/build.gradle b/core/build.gradle index 8b13789..55c170e 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -1 +1,10 @@ +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/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 new file mode 100644 index 0000000..b0e664a --- /dev/null +++ b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowAuthenticator.java @@ -0,0 +1,247 @@ +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; +import com.google.gson.JsonSyntaxException; +import com.google.gson.annotations.SerializedName; +import java.io.IOException; +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; +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; + private KeyFlowTokenResponse token; + private final Gson gson; + private final String tokenUrl; + private long tokenLeewayInSeconds = DEFAULT_TOKEN_LEEWAY; + + protected 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 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(); + } + + 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 + */ + public KeyFlowAuthenticator( + CoreConfiguration cfg, + ServiceAccountKey saKey, + EnvironmentVariables environmentVariables) { + this.saKey = saKey; + this.gson = new Gson(); + this.httpClient = + new OkHttpClient.Builder() + .connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS) + .writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS) + .readTimeout(READ_TIMEOUT, TimeUnit.SECONDS) + .build(); + + 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(); + } + } + + /** + * 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, InvalidKeySpecException { + if (token == null) { + createAccessToken(); + } else if (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 + */ + protected 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 + */ + protected 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, IOException { + 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 || response.body().contentLength() == 0) { + throw new JsonSyntaxException("body from token creation is null"); + } + + KeyFlowTokenResponse keyFlowTokenResponse = + gson.fromJson( + new InputStreamReader(response.body().byteStream(), StandardCharsets.UTF_8), + KeyFlowTokenResponse.class); + setToken(keyFlowTokenResponse); + 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); + } + + 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; + + 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 new file mode 100644 index 0000000..b9fdafd --- /dev/null +++ b/core/src/main/java/cloud/stackit/sdk/core/KeyFlowInterceptor.java @@ -0,0 +1,38 @@ +package cloud.stackit.sdk.core; + +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; +import org.jetbrains.annotations.NotNull; + +public class KeyFlowInterceptor implements Interceptor { + private final KeyFlowAuthenticator 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 (InvalidKeySpecException | 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); + } +} 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..c23eaea --- /dev/null +++ b/core/src/main/java/cloud/stackit/sdk/core/auth/SetupAuth.java @@ -0,0 +1,299 @@ +package cloud.stackit.sdk.core.auth; + +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.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; +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.util.Map; +import javax.swing.filechooser.FileSystemView; +import okhttp3.Interceptor; + +public class SetupAuth { + private final EnvironmentVariables env; + private Interceptor authHandler; + private final CoreConfiguration cfg; + 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 CredentialsInFileNotFoundException when no configuration is set or can be found + */ + public SetupAuth() throws CredentialsInFileNotFoundException { + this(new CoreConfiguration.Builder().build(), 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 IOException when a file can be found + * @throws CredentialsInFileNotFoundException when no credentials are set or can be found + */ + 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; + } + + /** + * 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 + */ + protected ServiceAccountKey setupKeyFlow(CoreConfiguration cfg) + throws CredentialsInFileNotFoundException, IOException { + // Explicit config in code + if (Utils.isStringSet(cfg.getServiceAccountKey())) { + ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(cfg.getServiceAccountKey()); + loadPrivateKey(cfg, saKey); + return saKey; + } + + if (Utils.isStringSet(cfg.getServiceAccountKeyPath())) { + 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 (Utils.isStringSet(env.getStackitServiceAccountKey())) { + ServiceAccountKey saKey = + ServiceAccountKey.loadFromJson(env.getStackitServiceAccountKey().trim()); + loadPrivateKey(cfg, saKey); + return saKey; + } + + if (Utils.isStringSet(env.getStackitServiceAccountKeyPath())) { + String fileContent = + new String( + Files.readAllBytes(Paths.get(env.getStackitServiceAccountKeyPath())), + StandardCharsets.UTF_8); + ServiceAccountKey saKey = ServiceAccountKey.loadFromJson(fileContent); + 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; + } + + protected 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 + // Get private key + if (Utils.isStringSet(cfg.getPrivateKey())) { + return cfg.getPrivateKey(); + } + // Get private key path + if (Utils.isStringSet(cfg.getPrivateKeyPath())) { + String privateKeyPath = cfg.getPrivateKeyPath(); + return new String( + Files.readAllBytes(Paths.get(privateKeyPath)), StandardCharsets.UTF_8); + } + // Get credentials file + if (Utils.isStringSet(cfg.getCredentialsFilePath())) { + return readValueFromCredentialsFile( + cfg.getCredentialsFilePath(), + EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY, + EnvironmentVariables.ENV_STACKIT_PRIVATE_KEY_PATH); + } + + // 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() + : defaultCredentialsFilePath; + + return readValueFromCredentialsFile( + credentialsFilePath, + 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 + */ + protected 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 (Utils.isStringSet(key)) { + return key; + } + + // Read KEY_PATH from credentials file + String keyPath = null; + try { + keyPath = (String) map.get(pathKey); + } catch (ClassCastException ignored) { + } + if (Utils.isStringSet(keyPath)) { + 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 new file mode 100644 index 0000000..f100526 --- /dev/null +++ b/core/src/main/java/cloud/stackit/sdk/core/config/CoreConfiguration.java @@ -0,0 +1,124 @@ +package cloud.stackit.sdk.core.config; + +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); + } + } +} 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..42b920e --- /dev/null +++ b/core/src/main/java/cloud/stackit/sdk/core/config/EnvironmentVariables.java @@ -0,0 +1,35 @@ +package cloud.stackit.sdk.core.config; + +public class EnvironmentVariables { + 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 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); + } + + public String getStackitCredentialsPath() { + return System.getenv(ENV_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 new file mode 100644 index 0000000..d7d6210 --- /dev/null +++ b/core/src/main/java/cloud/stackit/sdk/core/exception/ApiException.java @@ -0,0 +1,173 @@ +package cloud.stackit.sdk.core.exception; + +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/exception/CredentialsInFileNotFoundException.java b/core/src/main/java/cloud/stackit/sdk/core/exception/CredentialsInFileNotFoundException.java new file mode 100644 index 0000000..052fc01 --- /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..365ea8e --- /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/ServiceAccountCredentials.java b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountCredentials.java new file mode 100644 index 0000000..fca5510 --- /dev/null +++ b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountCredentials.java @@ -0,0 +1,84 @@ +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; + 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 String getAud() { + return aud; + } + + public String getIss() { + return iss; + } + + public String getKid() { + return kid; + } + + public String getPrivateKey() { + return privateKey; + } + + public void setPrivateKey(String privateKey) { + this.privateKey = privateKey; + } + + public boolean isPrivateKeySet() { + return Utils.isStringSet(privateKey); + } + + 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", ""); + + byte[] privateBytes = Base64.getDecoder().decode(trimmedKey); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateBytes); + 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 new file mode 100644 index 0000000..a84adf6 --- /dev/null +++ b/core/src/main/java/cloud/stackit/sdk/core/model/ServiceAccountKey.java @@ -0,0 +1,118 @@ +package cloud.stackit.sdk.core.model; + +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; + 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 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; + } + + @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/config/CoreConfigurationTest.java b/core/src/test/java/cloud/stackit/sdk/core/config/CoreConfigurationTest.java new file mode 100644 index 0000000..4c0e37b --- /dev/null +++ b/core/src/test/java/cloud/stackit/sdk/core/config/CoreConfigurationTest.java @@ -0,0 +1,186 @@ +package cloud.stackit.sdk.core.config; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.HashMap; +import java.util.Map; +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(); + + 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); + } +} 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..0685ea7 --- /dev/null +++ b/core/src/test/java/cloud/stackit/sdk/core/model/ServiceAccountCredentialsTest.java @@ -0,0 +1,77 @@ +package cloud.stackit.sdk.core.model; + +import static org.junit.jupiter.api.Assertions.*; + +import java.security.spec.InvalidKeySpecException; +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); + } +} 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..2909742 --- /dev/null +++ b/core/src/test/java/cloud/stackit/sdk/core/model/ServiceAccountKeyTest.java @@ -0,0 +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; + +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()); + } +} 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/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 7170f77..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 @@ -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,6 +121,26 @@ 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); + auth.init(); + List interceptors = new LinkedList<>(); + interceptors.add(auth.getAuthHandler()); + initHttpClient(interceptors); + } + protected void initHttpClient() { initHttpClient(Collections.emptyList()); } 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..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 @@ -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; @@ -32,8 +33,10 @@ import cloud.stackit.sdk.resourcemanager.model.PartialUpdateProjectPayload; import cloud.stackit.sdk.resourcemanager.model.Project; import com.google.gson.reflect.TypeToken; +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; @@ -53,6 +56,18 @@ 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 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; }