From 50a36f1ebeb965f388ec2ce6a9c736461eba403f Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 28 Oct 2025 14:52:18 +0400 Subject: [PATCH 1/4] fix: add backwards compatibility for >= Java 8 --- pom.xml | 32 ++-- .../java/com/infisical/sdk/api/ApiClient.java | 48 +++--- .../infisical/sdk/auth/AwsAuthProvider.java | 151 ++++++++++-------- .../infisical/sdk/resources/AuthClient.java | 21 ++- .../sdk/resources/FoldersClient.java | 42 +++-- .../sdk/resources/SecretsClient.java | 130 +++++++-------- .../com/infisical/sdk/InfisicalSdkTest.java | 48 +++--- .../sdk/auth/AwsAuthProviderTest.java | 118 ++++++++------ 8 files changed, 313 insertions(+), 277 deletions(-) diff --git a/pom.xml b/pom.xml index a13a913..09204b2 100644 --- a/pom.xml +++ b/pom.xml @@ -1,10 +1,13 @@ - + org.springframework.boot spring-boot-starter-parent 3.1.3 - + 4.0.0 com.infisical @@ -32,7 +35,8 @@ scm:git:git://github.com/infisical/java-sdk.git - scm:git:ssh://git@github.com/infiscial/java-sdk.git + scm:git:ssh://git@github.com/infiscial/java-sdk.git https://github.com/infisical/java-sdk/ @@ -48,7 +52,12 @@ - 21 + 8 + 8 + 8 + + + 21 2.7.5 2.8.9 @@ -82,20 +91,20 @@ ch.qos.logback logback-core - 1.5.19 + 1.3.14 org.slf4j slf4j-api - 2.1.0-alpha1 + 2.0.9 test ch.qos.logback logback-classic - 1.5.19 + 1.3.14 @@ -152,7 +161,7 @@ maven-compiler-plugin 3.11.0 - 21 + 8 org.projectlombok @@ -182,7 +191,7 @@ maven-compiler-plugin 3.11.0 - 21 + 8 org.projectlombok @@ -235,7 +244,8 @@ --pinentry-mode loopback - ${env.MAVEN_GPG_PASSPHRASE} + ${env.MAVEN_GPG_PASSPHRASE} diff --git a/src/main/java/com/infisical/sdk/api/ApiClient.java b/src/main/java/com/infisical/sdk/api/ApiClient.java index 7969360..e417240 100644 --- a/src/main/java/com/infisical/sdk/api/ApiClient.java +++ b/src/main/java/com/infisical/sdk/api/ApiClient.java @@ -3,6 +3,7 @@ import com.google.gson.Gson; import com.infisical.sdk.util.InfisicalException; import com.squareup.okhttp.*; +import com.squareup.okhttp.Request; import java.io.IOException; import java.util.Map; @@ -69,20 +70,19 @@ private String formatErrorMessage(Response response) throws IOException { public R post(String url, T requestBody, Class responseType) throws InfisicalException { try { // convert request to json - var gson = new Gson(); + Gson gson = new Gson(); String jsonBody = gson.toJson(requestBody); // Build request - var requestBuilder = - new Request.Builder() - .url(url) - .post(RequestBody.create(JSON, jsonBody)) - .header("Accept", "application/json"); + Request.Builder requestBuilder = new Request.Builder() + .url(url) + .post(RequestBody.create(JSON, jsonBody)) + .header("Accept", "application/json"); if (this.accessToken != null && !this.accessToken.isEmpty()) { requestBuilder.addHeader("Authorization", "Bearer " + this.accessToken); } - var request = requestBuilder.build(); + Request request = requestBuilder.build(); Response response = client.newCall(request).execute(); try (ResponseBody responseBody = response.body()) { @@ -111,14 +111,14 @@ public R get(String baseUrl, Map queryParams, Class respo queryParams.forEach(urlBuilder::addQueryParameter); } - var requestBuilder = - new Request.Builder().url(urlBuilder.build()).get().header("Accept", "application/json"); + Request.Builder requestBuilder = new Request.Builder().url(urlBuilder.build()).get().header("Accept", + "application/json"); if (this.accessToken != null && !this.accessToken.isEmpty()) { requestBuilder.addHeader("Authorization", "Bearer " + this.accessToken); } - var request = requestBuilder.build(); + Request request = requestBuilder.build(); Response response = client.newCall(request).execute(); try (ResponseBody responseBody = response.body()) { @@ -130,7 +130,7 @@ public R get(String baseUrl, Map queryParams, Class respo throw new IOException("Response body is null"); } - var gson = new Gson(); + Gson gson = new Gson(); String responseJson = responseBody.string(); return gson.fromJson(responseJson, responseType); } @@ -143,20 +143,19 @@ public R patch(String url, T requestBody, Class responseType) throws InfisicalException { try { // convert request to json - var gson = new Gson(); + Gson gson = new Gson(); String jsonBody = gson.toJson(requestBody); // Build request - var requestBuilder = - new Request.Builder() - .url(url) - .patch(RequestBody.create(JSON, jsonBody)) - .header("Accept", "application/json"); + Request.Builder requestBuilder = new Request.Builder() + .url(url) + .patch(RequestBody.create(JSON, jsonBody)) + .header("Accept", "application/json"); if (this.accessToken != null && !this.accessToken.isEmpty()) { requestBuilder.addHeader("Authorization", "Bearer " + this.accessToken); } - var request = requestBuilder.build(); + Request request = requestBuilder.build(); Response response = client.newCall(request).execute(); try (ResponseBody responseBody = response.body()) { @@ -180,20 +179,19 @@ public R delete(String url, T requestBody, Class responseType) throws InfisicalException { try { // convert request to json - var gson = new Gson(); + Gson gson = new Gson(); String jsonBody = gson.toJson(requestBody); // Build request - var requestBuilder = - new Request.Builder() - .url(url) - .delete(RequestBody.create(JSON, jsonBody)) - .header("Accept", "application/json"); + Request.Builder requestBuilder = new Request.Builder() + .url(url) + .delete(RequestBody.create(JSON, jsonBody)) + .header("Accept", "application/json"); if (this.accessToken != null && !this.accessToken.isEmpty()) { requestBuilder.addHeader("Authorization", "Bearer " + this.accessToken); } - var request = requestBuilder.build(); + Request request = requestBuilder.build(); Response response = client.newCall(request).execute(); try (ResponseBody responseBody = response.body()) { diff --git a/src/main/java/com/infisical/sdk/auth/AwsAuthProvider.java b/src/main/java/com/infisical/sdk/auth/AwsAuthProvider.java index aa6a337..0b10d08 100644 --- a/src/main/java/com/infisical/sdk/auth/AwsAuthProvider.java +++ b/src/main/java/com/infisical/sdk/auth/AwsAuthProvider.java @@ -3,13 +3,18 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.infisical.sdk.models.AwsAuthParameters; + +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.time.Clock; import java.time.Instant; import java.time.ZoneOffset; +import java.util.AbstractMap; import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -26,6 +31,8 @@ import software.amazon.awssdk.http.auth.aws.signer.AwsV4FamilyHttpSigner; import software.amazon.awssdk.http.auth.aws.signer.AwsV4HttpSigner; import software.amazon.awssdk.http.auth.spi.signer.HttpSigner; +import software.amazon.awssdk.http.auth.spi.signer.SignRequest; +import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain; @@ -34,78 +41,87 @@ public class AwsAuthProvider { private static final ObjectMapper objectMapper = new ObjectMapper(); - @NonNull @Builder.Default private final String serviceName = "sts"; - @NonNull @Builder.Default private final SdkHttpMethod httpMethod = SdkHttpMethod.POST; - @NonNull @Builder.Default private final String endpointTemplate = "https://sts.%s.amazonaws.com"; + @NonNull + @Builder.Default + private final String serviceName = "sts"; + @NonNull + @Builder.Default + private final SdkHttpMethod httpMethod = SdkHttpMethod.POST; + @NonNull + @Builder.Default + private final String endpointTemplate = "https://sts.%s.amazonaws.com"; - @NonNull @Builder.Default + @NonNull + @Builder.Default private final String contentType = "application/x-www-form-urlencoded; charset=utf-8"; - @NonNull @Builder.Default - private final Map> params = - Map.ofEntries( - Map.entry("Action", List.of("GetCallerIdentity")), - Map.entry("Version", List.of("2011-06-15"))); + @NonNull + @Builder.Default + private final Map> params = createParams(); private final Instant overrideInstant; + private static Map> createParams() { + Map> map = new HashMap<>(); + map.put("Action", Collections.singletonList("GetCallerIdentity")); + map.put("Version", Collections.singletonList("2011-06-15")); + return Collections.unmodifiableMap(map); + } + /** * Create AwsAuthLoginInput from given AWS credentials. * - * @param region region of AWS identity - * @param credentials AWS credentials for creating the login input + * @param region region of AWS identity + * @param credentials AWS credentials for creating the login input * @param sessionToken Session token for creating the login input - * @return the AwsAuthLoginInput created from the given credentials for exchanging access token + * @return the AwsAuthLoginInput created from the given credentials for + * exchanging access token */ public AwsAuthParameters fromCredentials( String region, AwsCredentials credentials, String sessionToken) { final AwsV4HttpSigner signer = AwsV4HttpSigner.create(); - final String iamRequestURL = endpointTemplate.formatted(region); + final String iamRequestURL = String.format(endpointTemplate, region); final String iamRequestBody = encodeParameters(params); - final SdkHttpFullRequest.Builder requestBuilder = - SdkHttpFullRequest.builder() - .uri(URI.create(iamRequestURL)) - .method(httpMethod) - .appendHeader("Content-Type", contentType); + final SdkHttpFullRequest.Builder requestBuilder = SdkHttpFullRequest.builder() + .uri(URI.create(iamRequestURL)) + .method(httpMethod) + .appendHeader("Content-Type", contentType); if (sessionToken != null) { requestBuilder.appendHeader("X-Amz-Security-Token", sessionToken); } final SdkHttpFullRequest request = requestBuilder.build(); - final SdkHttpRequest signedRequest = - signer - .sign( - signingRequest -> { - var req = - signingRequest - .request(request) - .identity(credentials) - .payload( - ContentStreamProvider.fromByteArray( - iamRequestBody.getBytes(StandardCharsets.UTF_8))) - .putProperty(AwsV4FamilyHttpSigner.SERVICE_SIGNING_NAME, serviceName) - .putProperty(AwsV4HttpSigner.REGION_NAME, region); - if (overrideInstant != null) { - req.putProperty( - HttpSigner.SIGNING_CLOCK, Clock.fixed(overrideInstant, ZoneOffset.UTC)); - } - }) - .request(); - final Map requestHeaders = - signedRequest.headers().entrySet().stream() - .map(entry -> Map.entry(entry.getKey(), entry.getValue().getFirst())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + final SdkHttpRequest signedRequest = signer + .sign( + signingRequest -> { + SignRequest.Builder req = signingRequest + .request(request) + .identity(credentials) + .payload( + ContentStreamProvider.fromByteArray( + iamRequestBody.getBytes(StandardCharsets.UTF_8))) + .putProperty(AwsV4FamilyHttpSigner.SERVICE_SIGNING_NAME, serviceName) + .putProperty(AwsV4HttpSigner.REGION_NAME, region); + if (overrideInstant != null) { + req.putProperty( + HttpSigner.SIGNING_CLOCK, Clock.fixed(overrideInstant, ZoneOffset.UTC)); + } + }) + .request(); + + final Map requestHeaders = signedRequest.headers().entrySet().stream() + .map(entry -> new AbstractMap.SimpleEntry<>(entry.getKey(), entry.getValue().get(0))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + requestHeaders.put("Content-Length", String.valueOf(iamRequestBody.length())); final String encodedHeader; try { - encodedHeader = - Base64.getEncoder() - .encodeToString( - objectMapper.writeValueAsString(requestHeaders).getBytes(StandardCharsets.UTF_8)); + encodedHeader = Base64.getEncoder() + .encodeToString( + objectMapper.writeValueAsString(requestHeaders).getBytes(StandardCharsets.UTF_8)); } catch (JsonProcessingException e) { throw new RuntimeException(e); } - final String encodedBody = - Base64.getEncoder().encodeToString(iamRequestBody.getBytes(StandardCharsets.UTF_8)); + final String encodedBody = Base64.getEncoder().encodeToString(iamRequestBody.getBytes(StandardCharsets.UTF_8)); return AwsAuthParameters.builder() .iamHttpRequestMethod(httpMethod.name()) .iamRequestHeaders(encodedHeader) @@ -114,18 +130,17 @@ public AwsAuthParameters fromCredentials( } /** - * Create AwsAuthLoginInput from the instance profile in the current environment. + * Create AwsAuthLoginInput from the instance profile in the current + * environment. * - * @return the AwsAuthLoginInput created from the current instance profile for exchanging access - * token + * @return the AwsAuthLoginInput created from the current instance profile for + * exchanging access + * token */ public AwsAuthParameters fromInstanceProfile() { - try (InstanceProfileCredentialsProvider provider = - InstanceProfileCredentialsProvider.create()) { - final AwsSessionCredentials credentials = - (AwsSessionCredentials) provider.resolveCredentials(); - final DefaultAwsRegionProviderChain regionProvider = - DefaultAwsRegionProviderChain.builder().build(); + try (InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.create()) { + final AwsSessionCredentials credentials = (AwsSessionCredentials) provider.resolveCredentials(); + final DefaultAwsRegionProviderChain regionProvider = DefaultAwsRegionProviderChain.builder().build(); final Region region = regionProvider.getRegion(); final String sessionToken = credentials.sessionToken(); return fromCredentials(region.id(), credentials, sessionToken); @@ -133,23 +148,27 @@ public AwsAuthParameters fromInstanceProfile() { } /** - * Encode given parameters with URL encoding for the body of form posting request. + * Encode given parameters with URL encoding for the body of form posting + * request. * * @param params parameters mapping key to values to encode * @return URL-encoded string of the parameters */ public static String encodeParameters(Map> params) { return params.entrySet().stream() - .flatMap(entry -> entry.getValue().stream().map(item -> Map.entry(entry.getKey(), item))) - // Notice: this is not really needed for real world usage, but it makes the - // body encoded in a deterministic order, so that unit test is much easier + .flatMap(entry -> entry.getValue().stream() + .map(item -> new AbstractMap.SimpleEntry<>(entry.getKey(), item))) .sorted(Map.Entry.comparingByKey()) - .map( - entry -> - String.format( - "%s=%s", - URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8), - URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8))) + .map(entry -> { + try { + return String.format( + "%s=%s", + URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8.name()), + URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8.name())); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + }) .collect(Collectors.joining("&")); } diff --git a/src/main/java/com/infisical/sdk/resources/AuthClient.java b/src/main/java/com/infisical/sdk/resources/AuthClient.java index bf334df..38cac5d 100644 --- a/src/main/java/com/infisical/sdk/resources/AuthClient.java +++ b/src/main/java/com/infisical/sdk/resources/AuthClient.java @@ -19,24 +19,23 @@ public AuthClient(ApiClient apiClient, Consumer onAuthenticate) { } public void UniversalAuthLogin(String clientId, String clientSecret) throws InfisicalException { - var params = - UniversalAuthLoginInput.builder().clientId(clientId).clientSecret(clientSecret).build(); + UniversalAuthLoginInput params = UniversalAuthLoginInput.builder().clientId(clientId).clientSecret(clientSecret) + .build(); - var url = - String.format("%s%s", this.apiClient.GetBaseUrl(), "/api/v1/auth/universal-auth/login"); - var credential = this.apiClient.post(url, params, MachineIdentityCredential.class); + String url = String.format("%s%s", this.apiClient.GetBaseUrl(), "/api/v1/auth/universal-auth/login"); + MachineIdentityCredential credential = this.apiClient.post(url, params, MachineIdentityCredential.class); this.onAuthenticate.accept(credential.getAccessToken()); } public void LdapAuthLogin(LdapAuthLoginInput input) throws InfisicalException { - var validationMsg = input.validate(); + String validationMsg = input.validate(); if (validationMsg != null) { throw new InfisicalException(validationMsg); } - var url = String.format("%s%s", this.apiClient.GetBaseUrl(), "/api/v1/auth/ldap-auth/login"); - var credential = this.apiClient.post(url, input, MachineIdentityCredential.class); + String url = String.format("%s%s", this.apiClient.GetBaseUrl(), "/api/v1/auth/ldap-auth/login"); + MachineIdentityCredential credential = this.apiClient.post(url, input, MachineIdentityCredential.class); this.onAuthenticate.accept(credential.getAccessToken()); } @@ -45,14 +44,14 @@ public void AwsAuthLogin(String identityId) throws InfisicalException { } public void AwsAuthLogin(AwsAuthLoginInput input) throws InfisicalException { - var validationMsg = input.validate(); + String validationMsg = input.validate(); if (validationMsg != null) { throw new InfisicalException(validationMsg); } - var url = String.format("%s%s", this.apiClient.GetBaseUrl(), "/api/v1/auth/aws-auth/login"); - var credential = this.apiClient.post(url, input, MachineIdentityCredential.class); + String url = String.format("%s%s", this.apiClient.GetBaseUrl(), "/api/v1/auth/aws-auth/login"); + MachineIdentityCredential credential = this.apiClient.post(url, input, MachineIdentityCredential.class); this.onAuthenticate.accept(credential.getAccessToken()); } diff --git a/src/main/java/com/infisical/sdk/resources/FoldersClient.java b/src/main/java/com/infisical/sdk/resources/FoldersClient.java index 0eeeec8..537b2df 100644 --- a/src/main/java/com/infisical/sdk/resources/FoldersClient.java +++ b/src/main/java/com/infisical/sdk/resources/FoldersClient.java @@ -15,9 +15,9 @@ public FoldersClient(ApiClient apiClient) { } public Folder Create(CreateFolderInput input) throws InfisicalException { - var url = String.format("%s%s", this.httpClient.GetBaseUrl(), "/api/v1/folders"); + String url = String.format("%s%s", this.httpClient.GetBaseUrl(), "/api/v1/folders"); - var result = this.httpClient.post(url, input, SingleFolderResponse.class); + SingleFolderResponse result = this.httpClient.post(url, input, SingleFolderResponse.class); return result.getFolder(); } @@ -27,58 +27,54 @@ public Folder Get(String folderId) throws InfisicalException { throw new InfisicalException("Folder ID is required"); } - var url = - String.format( - "%s%s", this.httpClient.GetBaseUrl(), String.format("/api/v1/folders/%s", folderId)); - var result = this.httpClient.get(url, null, SingleFolderResponse.class); + String url = String.format( + "%s%s", this.httpClient.GetBaseUrl(), String.format("/api/v1/folders/%s", folderId)); + SingleFolderResponse result = this.httpClient.get(url, null, SingleFolderResponse.class); return result.getFolder(); } public List List(ListFoldersInput input) throws InfisicalException { - var validationMsg = input.validate(); + String validationMsg = input.validate(); if (validationMsg != null) { throw new InfisicalException(validationMsg); } - var url = String.format("%s%s", this.httpClient.GetBaseUrl(), "/api/v1/folders"); - var result = - this.httpClient.get( - url, ObjectToMapConverter.toStringMap(input), ListFoldersResponse.class); + String url = String.format("%s%s", this.httpClient.GetBaseUrl(), "/api/v1/folders"); + ListFoldersResponse result = this.httpClient.get( + url, ObjectToMapConverter.toStringMap(input), ListFoldersResponse.class); return result.getFolders(); } public Folder Update(UpdateFolderInput input) throws InfisicalException { - var validationMsg = input.validate(); + String validationMsg = input.validate(); if (validationMsg != null) { throw new InfisicalException(validationMsg); } - var url = - String.format( - "%s%s", - this.httpClient.GetBaseUrl(), String.format("/api/v1/folders/%s", input.getFolderId())); + String url = String.format( + "%s%s", + this.httpClient.GetBaseUrl(), String.format("/api/v1/folders/%s", input.getFolderId())); - var result = this.httpClient.patch(url, input, SingleFolderResponse.class); + SingleFolderResponse result = this.httpClient.patch(url, input, SingleFolderResponse.class); return result.getFolder(); } public Folder Delete(DeleteFolderInput input) throws InfisicalException { - var validationMsg = input.validate(); + String validationMsg = input.validate(); if (validationMsg != null) { throw new InfisicalException(validationMsg); } - var url = - String.format( - "%s%s", - this.httpClient.GetBaseUrl(), String.format("/api/v1/folders/%s", input.getFolderId())); + String url = String.format( + "%s%s", + this.httpClient.GetBaseUrl(), String.format("/api/v1/folders/%s", input.getFolderId())); - var result = this.httpClient.delete(url, input, SingleFolderResponse.class); + SingleFolderResponse result = this.httpClient.delete(url, input, SingleFolderResponse.class); return result.getFolder(); } diff --git a/src/main/java/com/infisical/sdk/resources/SecretsClient.java b/src/main/java/com/infisical/sdk/resources/SecretsClient.java index 4930df0..7f3293c 100644 --- a/src/main/java/com/infisical/sdk/resources/SecretsClient.java +++ b/src/main/java/com/infisical/sdk/resources/SecretsClient.java @@ -11,6 +11,7 @@ import com.infisical.sdk.util.Helper; import com.infisical.sdk.util.InfisicalException; import java.util.List; +import java.util.Map; public class SecretsClient { private final ApiClient httpClient; @@ -48,19 +49,18 @@ public List ListSecrets( Boolean includeImports, Boolean setSecretsOnSystemProperties) throws InfisicalException { - var url = String.format("%s%s", this.httpClient.GetBaseUrl(), "/api/v3/secrets/raw"); + String url = String.format("%s%s", this.httpClient.GetBaseUrl(), "/api/v3/secrets/raw"); - var queryParameters = - new QueryBuilder() - .add("workspaceId", projectId) - .add("environment", environmentSlug) - .add("secretPath", secretPath) - .add("expandSecretReferences", Helper.booleanToString(expandSecretReferences)) - .add("recursive", Helper.booleanToString(recursive)) - .add("includeImports", Helper.booleanToString(includeImports)) - .build(); + Map queryParameters = new QueryBuilder() + .add("workspaceId", projectId) + .add("environment", environmentSlug) + .add("secretPath", secretPath) + .add("expandSecretReferences", Helper.booleanToString(expandSecretReferences)) + .add("recursive", Helper.booleanToString(recursive)) + .add("includeImports", Helper.booleanToString(includeImports)) + .build(); - var listSecrets = this.httpClient.get(url, queryParameters, ListSecretsResponse.class); + ListSecretsResponse listSecrets = this.httpClient.get(url, queryParameters, ListSecretsResponse.class); if (setSecretsOnSystemProperties) { for (Secret secret : listSecrets.getSecrets()) { @@ -80,22 +80,20 @@ public Secret GetSecret( Boolean includeImports, String secretType) throws InfisicalException { - var url = - String.format( - "%s%s", - this.httpClient.GetBaseUrl(), String.format("/api/v3/secrets/raw/%s", secretName)); - - var queryParameters = - new QueryBuilder() - .add("workspaceId", projectId) - .add("environment", environmentSlug) - .add("secretPath", secretPath) - .add("expandSecretReferences", Helper.booleanToString(expandSecretReferences)) - .add("includeImports", Helper.booleanToString(includeImports)) - .add("type", secretType) - .build(); - - var result = this.httpClient.get(url, queryParameters, SingleSecretResponse.class); + String url = String.format( + "%s%s", + this.httpClient.GetBaseUrl(), String.format("/api/v3/secrets/raw/%s", secretName)); + + Map queryParameters = new QueryBuilder() + .add("workspaceId", projectId) + .add("environment", environmentSlug) + .add("secretPath", secretPath) + .add("expandSecretReferences", Helper.booleanToString(expandSecretReferences)) + .add("includeImports", Helper.booleanToString(includeImports)) + .add("type", secretType) + .build(); + + SingleSecretResponse result = this.httpClient.get(url, queryParameters, SingleSecretResponse.class); return result.getSecret(); } @@ -108,25 +106,25 @@ public Secret UpdateSecret( String newSecretValue, String newSecretName) throws InfisicalException { - var url = - String.format( - "%s%s", - this.httpClient.GetBaseUrl(), String.format("/api/v3/secrets/raw/%s", secretName)); + String url = String.format( + "%s%s", + this.httpClient.GetBaseUrl(), String.format("/api/v3/secrets/raw/%s", secretName)); - var inputBuilder = - UpdateSecretInput.builder() - .secretPath(secretPath) - .projectId(projectId) - .environmentSlug(environmentSlug) - .newSecretName(newSecretName) - .secretValue(newSecretValue); + UpdateSecretInput.UpdateSecretInputBuilder inputBuilder = UpdateSecretInput.builder() + .secretPath(secretPath) + .projectId(projectId) + .environmentSlug(environmentSlug) + .newSecretName(newSecretName) + .secretValue(newSecretValue); - if (newSecretName != null) inputBuilder.newSecretName(newSecretName); - if (newSecretName != null) inputBuilder.secretValue(newSecretValue); + if (newSecretName != null) + inputBuilder.newSecretName(newSecretName); + if (newSecretName != null) + inputBuilder.secretValue(newSecretValue); - var requestInput = inputBuilder.build(); + UpdateSecretInput requestInput = inputBuilder.build(); - var result = this.httpClient.patch(url, requestInput, SingleSecretResponse.class); + SingleSecretResponse result = this.httpClient.patch(url, requestInput, SingleSecretResponse.class); return result.getSecret(); } @@ -138,22 +136,20 @@ public Secret CreateSecret( String environmentSlug, String secretPath) throws InfisicalException { - var url = - String.format( - "%s%s", - this.httpClient.GetBaseUrl(), String.format("/api/v3/secrets/raw/%s", secretName)); - - var createSecretInput = - CreateSecretInput.builder() - .secretPath(secretPath) - .projectId(projectId) - .environmentSlug(environmentSlug) - .secretValue(secretValue) - .build(); + String url = String.format( + "%s%s", + this.httpClient.GetBaseUrl(), String.format("/api/v3/secrets/raw/%s", secretName)); + + CreateSecretInput createSecretInput = CreateSecretInput.builder() + .secretPath(secretPath) + .projectId(projectId) + .environmentSlug(environmentSlug) + .secretValue(secretValue) + .build(); createSecretInput.setSecretValue(!secretValue.isEmpty() ? secretValue : ""); - var result = this.httpClient.post(url, createSecretInput, SingleSecretResponse.class); + SingleSecretResponse result = this.httpClient.post(url, createSecretInput, SingleSecretResponse.class); return result.getSecret(); } @@ -161,19 +157,17 @@ public Secret CreateSecret( public Secret DeleteSecret( String secretName, String projectId, String environmentSlug, String secretPath) throws InfisicalException { - var url = - String.format( - "%s%s", - this.httpClient.GetBaseUrl(), String.format("/api/v3/secrets/raw/%s", secretName)); - - var deleteSecretInput = - DeleteSecretInput.builder() - .secretPath(secretPath) - .projectId(projectId) - .environmentSlug(environmentSlug) - .build(); - - var result = this.httpClient.delete(url, deleteSecretInput, SingleSecretResponse.class); + String url = String.format( + "%s%s", + this.httpClient.GetBaseUrl(), String.format("/api/v3/secrets/raw/%s", secretName)); + + DeleteSecretInput deleteSecretInput = DeleteSecretInput.builder() + .secretPath(secretPath) + .projectId(projectId) + .environmentSlug(environmentSlug) + .build(); + + SingleSecretResponse result = this.httpClient.delete(url, deleteSecretInput, SingleSecretResponse.class); return result.getSecret(); } } diff --git a/src/test/java/com/infisical/sdk/InfisicalSdkTest.java b/src/test/java/com/infisical/sdk/InfisicalSdkTest.java index 9311124..a2bbc6b 100644 --- a/src/test/java/com/infisical/sdk/InfisicalSdkTest.java +++ b/src/test/java/com/infisical/sdk/InfisicalSdkTest.java @@ -2,7 +2,10 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import java.util.List; + import com.infisical.sdk.config.SdkConfig; +import com.infisical.sdk.models.Secret; import com.infisical.sdk.util.EnvironmentVariables; import com.infisical.sdk.util.InfisicalException; import com.infisical.sdk.util.RandomUtil; @@ -15,9 +18,9 @@ public class InfisicalSdkTest { @Test public void TestListSecrets() { - var envVars = new EnvironmentVariables(); + EnvironmentVariables envVars = new EnvironmentVariables(); - var sdk = new InfisicalSdk(new SdkConfig.Builder().withSiteUrl(envVars.getSiteUrl()).build()); + InfisicalSdk sdk = new InfisicalSdk(new SdkConfig.Builder().withSiteUrl(envVars.getSiteUrl()).build()); assertDoesNotThrow( () -> { @@ -27,8 +30,7 @@ public void TestListSecrets() { }); try { - var secrets = - sdk.Secrets().ListSecrets(envVars.getProjectId(), "dev", "/", false, false, false, false); + List secrets = sdk.Secrets().ListSecrets(envVars.getProjectId(), "dev", "/", false, false, false, false); logger.info("Secrets length {}", secrets.size()); } catch (InfisicalException e) { @@ -38,9 +40,9 @@ public void TestListSecrets() { @Test public void TestGetSecret() { - var envVars = new EnvironmentVariables(); + EnvironmentVariables envVars = new EnvironmentVariables(); - var sdk = new InfisicalSdk(new SdkConfig.Builder().withSiteUrl(envVars.getSiteUrl()).build()); + InfisicalSdk sdk = new InfisicalSdk(new SdkConfig.Builder().withSiteUrl(envVars.getSiteUrl()).build()); assertDoesNotThrow( () -> { @@ -50,8 +52,7 @@ public void TestGetSecret() { }); try { - var secret = - sdk.Secrets().GetSecret("SECRET", envVars.getProjectId(), "dev", "/", null, null, null); + Secret secret = sdk.Secrets().GetSecret("SECRET", envVars.getProjectId(), "dev", "/", null, null, null); logger.info("TestGetSecret: Secret {}", secret); @@ -66,9 +67,9 @@ public void TestGetSecret() { @Test public void TestUpdateSecret() { - var envVars = new EnvironmentVariables(); + EnvironmentVariables envVars = new EnvironmentVariables(); - var sdk = new InfisicalSdk(new SdkConfig.Builder().withSiteUrl(envVars.getSiteUrl()).build()); + InfisicalSdk sdk = new InfisicalSdk(new SdkConfig.Builder().withSiteUrl(envVars.getSiteUrl()).build()); assertDoesNotThrow( () -> { @@ -78,9 +79,8 @@ public void TestUpdateSecret() { }); try { - var updatedSecret = - sdk.Secrets() - .UpdateSecret("SECRET", envVars.getProjectId(), "dev", "/", "new-value-123", null); + Secret updatedSecret = sdk.Secrets() + .UpdateSecret("SECRET", envVars.getProjectId(), "dev", "/", "new-value-123", null); logger.info("TestUpdateSecret: Secret {}", updatedSecret); @@ -91,9 +91,9 @@ public void TestUpdateSecret() { @Test public void TestCreateSecret() { - var envVars = new EnvironmentVariables(); + EnvironmentVariables envVars = new EnvironmentVariables(); - var sdk = new InfisicalSdk(new SdkConfig.Builder().withSiteUrl(envVars.getSiteUrl()).build()); + InfisicalSdk sdk = new InfisicalSdk(new SdkConfig.Builder().withSiteUrl(envVars.getSiteUrl()).build()); assertDoesNotThrow( () -> { @@ -103,11 +103,10 @@ public void TestCreateSecret() { }); try { - var secretValue = RandomUtil.generateRandomString(36); - var secretName = RandomUtil.generateRandomString(36); + String secretValue = RandomUtil.generateRandomString(36); + String secretName = RandomUtil.generateRandomString(36); - var secret = - sdk.Secrets().CreateSecret(secretName, secretValue, envVars.getProjectId(), "dev", "/"); + Secret secret = sdk.Secrets().CreateSecret(secretName, secretValue, envVars.getProjectId(), "dev", "/"); if (secret == null) { throw new AssertionError("Secret not found"); @@ -123,9 +122,9 @@ public void TestCreateSecret() { @Test public void TestDeleteSecret() { - var envVars = new EnvironmentVariables(); + EnvironmentVariables envVars = new EnvironmentVariables(); - var sdk = new InfisicalSdk(new SdkConfig.Builder().withSiteUrl(envVars.getSiteUrl()).build()); + InfisicalSdk sdk = new InfisicalSdk(new SdkConfig.Builder().withSiteUrl(envVars.getSiteUrl()).build()); assertDoesNotThrow( () -> { @@ -135,13 +134,12 @@ public void TestDeleteSecret() { }); try { - var secretValue = RandomUtil.generateRandomString(36); - var secretName = RandomUtil.generateRandomString(36); + String secretValue = RandomUtil.generateRandomString(36); + String secretName = RandomUtil.generateRandomString(36); sdk.Secrets().CreateSecret(secretName, secretValue, envVars.getProjectId(), "dev", "/"); - var deletedSecret = - sdk.Secrets().DeleteSecret(secretName, envVars.getProjectId(), "dev", "/"); + Secret deletedSecret = sdk.Secrets().DeleteSecret(secretName, envVars.getProjectId(), "dev", "/"); if (deletedSecret == null) { throw new AssertionError("Secret not found"); diff --git a/src/test/java/com/infisical/sdk/auth/AwsAuthProviderTest.java b/src/test/java/com/infisical/sdk/auth/AwsAuthProviderTest.java index 1545ac3..0ac426b 100644 --- a/src/test/java/com/infisical/sdk/auth/AwsAuthProviderTest.java +++ b/src/test/java/com/infisical/sdk/auth/AwsAuthProviderTest.java @@ -6,11 +6,16 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.infisical.sdk.models.AwsAuthParameters; + +import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.time.Instant; +import java.util.AbstractMap; import java.util.Arrays; import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -26,70 +31,87 @@ class AwsAuthProviderTest { @Test void testFromCredentials() throws JsonProcessingException { - final AwsAuthProvider provider = - AwsAuthProvider.builder().overrideInstant(Instant.ofEpochSecond(1759446719)).build(); - final AwsAuthParameters loginInput = - provider.fromCredentials( - "us-west-2", - AwsBasicCredentials.create("MOCK_ACCESS_KEY", "MOCK_SECRET_KEY"), - "MOCK_SESSION_TOKEN"); + final AwsAuthProvider provider = AwsAuthProvider.builder().overrideInstant(Instant.ofEpochSecond(1759446719)) + .build(); + final AwsAuthParameters loginInput = provider.fromCredentials( + "us-west-2", + AwsBasicCredentials.create("MOCK_ACCESS_KEY", "MOCK_SECRET_KEY"), + "MOCK_SESSION_TOKEN"); assertEquals("POST", loginInput.getIamHttpRequestMethod()); - final String decodedBody = - new String( - Base64.getDecoder().decode(loginInput.getIamRequestBody()), StandardCharsets.UTF_8); - final Map> bodyParams = - Arrays.stream(decodedBody.split("&")) - .map( - item -> { - final String[] parts = - URLDecoder.decode(item, StandardCharsets.UTF_8).split("=", 2); - return Map.entry(parts[0], List.of(parts[1])); - }) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + final String decodedBody = new String( + Base64.getDecoder().decode(loginInput.getIamRequestBody()), StandardCharsets.UTF_8); + final Map> bodyParams = Arrays.stream(decodedBody.split("&")) + .map(item -> { + try { + final String[] parts = URLDecoder.decode(item, StandardCharsets.UTF_8.name()).split("=", 2); + return new AbstractMap.SimpleEntry>( + parts[0], + Collections.singletonList(parts[1])); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + }) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + assertEquals(provider.getParams(), bodyParams); - final String decodedHeaders = - new String( - Base64.getDecoder().decode(loginInput.getIamRequestHeaders()), StandardCharsets.UTF_8); - final Map actualHeaders = - objectMapper.readValue(decodedHeaders, new TypeReference<>() {}); - assertEquals( - Map.ofEntries( - Map.entry("Host", "sts.us-west-2.amazonaws.com"), - Map.entry("Content-Type", "application/x-www-form-urlencoded; charset=utf-8"), - Map.entry("Content-Length", "43"), - Map.entry( - "x-amz-content-sha256", - "ab821ae955788b0e33ebd34c208442ccfc2d406e2edc5e7a39bd6458fbb4f843"), - Map.entry("X-Amz-Security-Token", "MOCK_SESSION_TOKEN"), - Map.entry("X-Amz-Date", "20251002T231159Z"), - Map.entry( - "Authorization", - "AWS4-HMAC-SHA256 Credential=MOCK_ACCESS_KEY/20251002/us-west-2/sts/aws4_request," - + " SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date;x-amz-security-token," - + " Signature=9b1b93454bea36297168ed67a861df12d17136f47cbdf5d23b1daa0fe704742b")), - actualHeaders); + final String decodedHeaders = new String( + Base64.getDecoder().decode(loginInput.getIamRequestHeaders()), StandardCharsets.UTF_8); + final Map actualHeaders = objectMapper.readValue(decodedHeaders, + new TypeReference>() { + }); + + Map expectedMap = new HashMap<>(); + expectedMap.put("Host", "sts.us-west-2.amazonaws.com"); + expectedMap.put("Content-Type", "application/x-www-form-urlencoded; charset=utf-8"); + expectedMap.put("Content-Length", "43"); + expectedMap.put( + "x-amz-content-sha256", + "ab821ae955788b0e33ebd34c208442ccfc2d406e2edc5e7a39bd6458fbb4f843"); + expectedMap.put("X-Amz-Security-Token", "MOCK_SESSION_TOKEN"); + expectedMap.put("X-Amz-Date", "20251002T231159Z"); + expectedMap.put( + "Authorization", + "AWS4-HMAC-SHA256 Credential=MOCK_ACCESS_KEY/20251002/us-west-2/sts/aws4_request," + + " SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date;x-amz-security-token," + + " Signature=9b1b93454bea36297168ed67a861df12d17136f47cbdf5d23b1daa0fe704742b"); + + assertEquals(expectedMap, actualHeaders); + } + + @SuppressWarnings("unchecked") + private static Map> createMap(Object... keyValues) { + if (keyValues.length % 2 != 0) { + throw new IllegalArgumentException("Must have even number of arguments"); + } + Map> map = new HashMap<>(); + for (int i = 0; i < keyValues.length; i += 2) { + map.put((String) keyValues[i], (List) keyValues[i + 1]); + } + return map; } static Stream encodeParametersCases() { return Stream.of( // empty - Arguments.of(Map.of(), ""), + Arguments.of(Collections.emptyMap(), ""), // simple Arguments.of( - Map.ofEntries(Map.entry("a", List.of("123")), Map.entry("b", List.of("456"))), + createMap( + "a", Collections.singletonList("123"), + "b", Collections.singletonList("456")), "a=123&b=456"), // sorting the key Arguments.of( - Map.ofEntries( - Map.entry("d", List.of("3")), - Map.entry("a", List.of("0")), - Map.entry("c", List.of("2")), - Map.entry("b", List.of("1"))), + createMap( + "d", Collections.singletonList("3"), + "a", Collections.singletonList("0"), + "c", Collections.singletonList("2"), + "b", Collections.singletonList("1")), "a=0&b=1&c=2&d=3"), Arguments.of( - Map.ofEntries(Map.entry("a", List.of("!@#$%^&*(){}[]"))), + createMap("a", Collections.singletonList("!@#$%^&*(){}[]")), "a=%21%40%23%24%25%5E%26*%28%29%7B%7D%5B%5D")); } From b608bfda33d2b5c60038ee119db7915a413bb4a6 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 28 Oct 2025 15:02:12 +0400 Subject: [PATCH 2/4] testing --- .../java/com/infisical/sdk/InfisicalSdkTest.java | 15 +++++---------- .../infisical/sdk/util/EnvironmentVariables.java | 11 ++++++----- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/test/java/com/infisical/sdk/InfisicalSdkTest.java b/src/test/java/com/infisical/sdk/InfisicalSdkTest.java index a2bbc6b..6ce7d75 100644 --- a/src/test/java/com/infisical/sdk/InfisicalSdkTest.java +++ b/src/test/java/com/infisical/sdk/InfisicalSdkTest.java @@ -25,8 +25,7 @@ public void TestListSecrets() { assertDoesNotThrow( () -> { sdk.Auth() - .UniversalAuthLogin( - envVars.getMachineIdentityClientId(), envVars.getMachineIdentityClientSecret()); + .AwsAuthLogin("f36f8795-d340-4dba-8b08-f98f8eb951f0"); }); try { @@ -47,8 +46,7 @@ public void TestGetSecret() { assertDoesNotThrow( () -> { sdk.Auth() - .UniversalAuthLogin( - envVars.getMachineIdentityClientId(), envVars.getMachineIdentityClientSecret()); + .AwsAuthLogin("f36f8795-d340-4dba-8b08-f98f8eb951f0"); }); try { @@ -74,8 +72,7 @@ public void TestUpdateSecret() { assertDoesNotThrow( () -> { sdk.Auth() - .UniversalAuthLogin( - envVars.getMachineIdentityClientId(), envVars.getMachineIdentityClientSecret()); + .AwsAuthLogin("f36f8795-d340-4dba-8b08-f98f8eb951f0"); }); try { @@ -98,8 +95,7 @@ public void TestCreateSecret() { assertDoesNotThrow( () -> { sdk.Auth() - .UniversalAuthLogin( - envVars.getMachineIdentityClientId(), envVars.getMachineIdentityClientSecret()); + .AwsAuthLogin("f36f8795-d340-4dba-8b08-f98f8eb951f0"); }); try { @@ -129,8 +125,7 @@ public void TestDeleteSecret() { assertDoesNotThrow( () -> { sdk.Auth() - .UniversalAuthLogin( - envVars.getMachineIdentityClientId(), envVars.getMachineIdentityClientSecret()); + .AwsAuthLogin("f36f8795-d340-4dba-8b08-f98f8eb951f0"); }); try { diff --git a/src/test/java/com/infisical/sdk/util/EnvironmentVariables.java b/src/test/java/com/infisical/sdk/util/EnvironmentVariables.java index 4e1af84..9b6aa1a 100644 --- a/src/test/java/com/infisical/sdk/util/EnvironmentVariables.java +++ b/src/test/java/com/infisical/sdk/util/EnvironmentVariables.java @@ -12,12 +12,13 @@ public EnvironmentVariables() throws RuntimeException { this.projectId = System.getenv("INFISICAL_PROJECT_ID"); this.siteUrl = System.getenv("INFISICAL_SITE_URL"); - if (machineIdentityClientId == null - || machineIdentityClientSecret == null - || projectId == null + if ( + // machineIdentityClientId == null + // || machineIdentityClientSecret == null + projectId == null || siteUrl == null - || machineIdentityClientId.isEmpty() - || machineIdentityClientSecret.isEmpty() + // || machineIdentityClientId.isEmpty() + // || machineIdentityClientSecret.isEmpty() || siteUrl.isEmpty() || projectId.isEmpty()) { throw new RuntimeException("One or more required environment variables are not set"); From 4e2e6814a07a5290a5e6779a8d29b5796674c319 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 28 Oct 2025 15:08:06 +0400 Subject: [PATCH 3/4] Revert "testing" This reverts commit b608bfda33d2b5c60038ee119db7915a413bb4a6. --- .../java/com/infisical/sdk/InfisicalSdkTest.java | 15 ++++++++++----- .../infisical/sdk/util/EnvironmentVariables.java | 11 +++++------ 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/test/java/com/infisical/sdk/InfisicalSdkTest.java b/src/test/java/com/infisical/sdk/InfisicalSdkTest.java index 6ce7d75..a2bbc6b 100644 --- a/src/test/java/com/infisical/sdk/InfisicalSdkTest.java +++ b/src/test/java/com/infisical/sdk/InfisicalSdkTest.java @@ -25,7 +25,8 @@ public void TestListSecrets() { assertDoesNotThrow( () -> { sdk.Auth() - .AwsAuthLogin("f36f8795-d340-4dba-8b08-f98f8eb951f0"); + .UniversalAuthLogin( + envVars.getMachineIdentityClientId(), envVars.getMachineIdentityClientSecret()); }); try { @@ -46,7 +47,8 @@ public void TestGetSecret() { assertDoesNotThrow( () -> { sdk.Auth() - .AwsAuthLogin("f36f8795-d340-4dba-8b08-f98f8eb951f0"); + .UniversalAuthLogin( + envVars.getMachineIdentityClientId(), envVars.getMachineIdentityClientSecret()); }); try { @@ -72,7 +74,8 @@ public void TestUpdateSecret() { assertDoesNotThrow( () -> { sdk.Auth() - .AwsAuthLogin("f36f8795-d340-4dba-8b08-f98f8eb951f0"); + .UniversalAuthLogin( + envVars.getMachineIdentityClientId(), envVars.getMachineIdentityClientSecret()); }); try { @@ -95,7 +98,8 @@ public void TestCreateSecret() { assertDoesNotThrow( () -> { sdk.Auth() - .AwsAuthLogin("f36f8795-d340-4dba-8b08-f98f8eb951f0"); + .UniversalAuthLogin( + envVars.getMachineIdentityClientId(), envVars.getMachineIdentityClientSecret()); }); try { @@ -125,7 +129,8 @@ public void TestDeleteSecret() { assertDoesNotThrow( () -> { sdk.Auth() - .AwsAuthLogin("f36f8795-d340-4dba-8b08-f98f8eb951f0"); + .UniversalAuthLogin( + envVars.getMachineIdentityClientId(), envVars.getMachineIdentityClientSecret()); }); try { diff --git a/src/test/java/com/infisical/sdk/util/EnvironmentVariables.java b/src/test/java/com/infisical/sdk/util/EnvironmentVariables.java index 9b6aa1a..4e1af84 100644 --- a/src/test/java/com/infisical/sdk/util/EnvironmentVariables.java +++ b/src/test/java/com/infisical/sdk/util/EnvironmentVariables.java @@ -12,13 +12,12 @@ public EnvironmentVariables() throws RuntimeException { this.projectId = System.getenv("INFISICAL_PROJECT_ID"); this.siteUrl = System.getenv("INFISICAL_SITE_URL"); - if ( - // machineIdentityClientId == null - // || machineIdentityClientSecret == null - projectId == null + if (machineIdentityClientId == null + || machineIdentityClientSecret == null + || projectId == null || siteUrl == null - // || machineIdentityClientId.isEmpty() - // || machineIdentityClientSecret.isEmpty() + || machineIdentityClientId.isEmpty() + || machineIdentityClientSecret.isEmpty() || siteUrl.isEmpty() || projectId.isEmpty()) { throw new RuntimeException("One or more required environment variables are not set"); From 8b6b189325c1cda7732bc8c895e4438dc115627d Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 28 Oct 2025 15:25:08 +0400 Subject: [PATCH 4/4] Update pom.xml --- pom.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/pom.xml b/pom.xml index 09204b2..9ad79c7 100644 --- a/pom.xml +++ b/pom.xml @@ -58,7 +58,6 @@ - 21 2.7.5 2.8.9 1.8.3