Skip to content

Commit a58e883

Browse files
authored
feat(clients): add generateSecuredApiKey to java (#3167)
1 parent e26dfbe commit a58e883

File tree

3 files changed

+124
-10
lines changed

3 files changed

+124
-10
lines changed

clients/algoliasearch-client-java/algoliasearch/src/main/java/com/algolia/internal/JsonSerializer.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,21 @@
1818
*/
1919
public final class JsonSerializer {
2020

21+
private static final ObjectMapper DEFAULT_OBJECT_MAPPER = new ObjectMapper()
22+
.enable(JsonGenerator.Feature.AUTO_CLOSE_JSON_CONTENT)
23+
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
24+
.disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
25+
.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
26+
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
27+
.disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
28+
.disable(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
29+
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
30+
.setSerializationInclusion(JsonInclude.Include.NON_NULL);
31+
32+
public static ObjectMapper getObjectMapper() {
33+
return DEFAULT_OBJECT_MAPPER;
34+
}
35+
2136
private final ObjectMapper mapper;
2237

2338
public static Builder builder() {

templates/java/api.mustache

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,32 @@ import java.util.function.IntUnaryOperator;
1919
import java.util.EnumSet;
2020
import java.util.Random;
2121
import java.util.Collections;
22-
{{^fullJavaUtil}}
2322
import java.util.ArrayList;
2423
import java.util.HashMap;
2524
import java.util.List;
2625
import java.util.Map;
2726
import java.util.UUID;
28-
{{/fullJavaUtil}}
2927
import java.util.stream.Collectors;
3028
import java.util.stream.Stream;
3129
import java.util.concurrent.CompletableFuture;
3230
import javax.annotation.Nonnull;
3331

32+
{{#isSearchClient}}
33+
import com.algolia.internal.JsonSerializer;
34+
import java.time.Duration;
35+
import java.time.Instant;
36+
import java.util.Base64;
37+
import java.util.regex.*;
38+
import com.algolia.search.models.apikeys.SecuredApiKeyRestriction;
39+
import java.nio.charset.Charset;
40+
import java.security.InvalidKeyException;
41+
import java.security.NoSuchAlgorithmException;
42+
import java.util.Base64;
43+
import javax.annotation.Nonnull;
44+
import javax.crypto.Mac;
45+
import javax.crypto.spec.SecretKeySpec;
46+
{{/isSearchClient}}
47+
3448
{{#operations}}
3549
public class {{classname}} extends ApiClient {
3650
{{#hasRegionalHost}}

templates/java/api_helpers.mustache

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -181,10 +181,10 @@ public GetApiKeyResponse waitForApiKey(
181181
}
182182
183183
/**
184-
* Helper: Wait for an API key to be added, updated or deleted based on a given `operation`.
184+
* Helper: Wait for an API key to be added or deleted based on a given `operation`.
185185
*
186186
* @param operation The `operation` that was done on a `key`. (ADD or DELETE only)
187-
* @param key The `key` that has been added, deleted or updated.
187+
* @param key The `key` that has been added or deleted.
188188
* @param maxRetries The maximum number of retry. 50 by default. (optional)
189189
* @param timeout The function to decide how long to wait between retries. min(retries * 200, 5000) by default. (optional)
190190
* @param requestOptions The requestOptions to send along with the query, they will be merged with the transporter requestOptions. (optional)
@@ -204,10 +204,10 @@ public GetApiKeyResponse waitForApiKey(ApiKeyOperation operation, String key, Ap
204204
return this.waitForApiKey(operation, key, apiKey, TaskUtils.DEFAULT_MAX_RETRIES, TaskUtils.DEFAULT_TIMEOUT, requestOptions);
205205
}
206206
/**
207-
* Helper: Wait for an API key to be added, updated or deleted based on a given `operation`.
207+
* Helper: Wait for an API key to be added or deleted based on a given `operation`.
208208
*
209209
* @param operation The `operation` that was done on a `key`. (ADD or DELETE only)
210-
* @param key The `key` that has been added, deleted or updated.
210+
* @param key The `key` that has been added or deleted.
211211
* @param requestOptions The requestOptions to send along with the query, they will be merged with the transporter requestOptions. (optional)
212212
*/
213213
public GetApiKeyResponse waitForApiKey(ApiKeyOperation operation, String key, RequestOptions requestOptions) {
@@ -226,10 +226,10 @@ public GetApiKeyResponse waitForApiKey(ApiKeyOperation operation, String key, Ap
226226
return this.waitForApiKey(operation, key, apiKey, maxRetries, timeout, null);
227227
}
228228
/**
229-
* Helper: Wait for an API key to be added, updated or deleted based on a given `operation`.
229+
* Helper: Wait for an API key to be added or deleted based on a given `operation`.
230230
*
231231
* @param operation The `operation` that was done on a `key`. (ADD or DELETE only)
232-
* @param key The `key` that has been added, deleted or updated.
232+
* @param key The `key` that has been added or deleted.
233233
* @param maxRetries The maximum number of retry. 50 by default. (optional)
234234
* @param timeout The function to decide how long to wait between retries. min(retries * 200, 5000) by default. (optional)
235235
*/
@@ -247,10 +247,10 @@ public GetApiKeyResponse waitForApiKey(ApiKeyOperation operation, String key, Ap
247247
return this.waitForApiKey(operation, key, apiKey, TaskUtils.DEFAULT_MAX_RETRIES, TaskUtils.DEFAULT_TIMEOUT, null);
248248
}
249249
/**
250-
* Helper: Wait for an API key to be added, updated or deleted based on a given `operation`.
250+
* Helper: Wait for an API key to be added or deleted based on a given `operation`.
251251
*
252252
* @param operation The `operation` that was done on a `key`. (ADD or DELETE only)
253-
* @param key The `key` that has been added, deleted or updated.
253+
* @param key The `key` that has been added or deleted.
254254
*/
255255
public GetApiKeyResponse waitForApiKey(ApiKeyOperation operation, String key) {
256256
return this.waitForApiKey(operation, key, null, TaskUtils.DEFAULT_MAX_RETRIES, TaskUtils.DEFAULT_TIMEOUT, null);
@@ -640,4 +640,89 @@ return new ReplaceAllObjectsResponse()
640640
.setBatchResponses(batchResponses)
641641
.setMoveOperationResponse(moveOperationResponse);
642642
}
643+
644+
/**
645+
* Helper: Generates a secured API key based on the given `parent_api_key` and given
646+
* `restrictions`.
647+
*
648+
* @param parentApiKey API key to generate from.
649+
* @param restrictions Restrictions to add the key
650+
* @throws Exception if an error occurs during the encoding
651+
* @throws AlgoliaRetryException When the retry has failed on all hosts
652+
* @throws AlgoliaApiException When the API sends an http error code
653+
* @throws AlgoliaRuntimeException When an error occurred during the serialization
654+
*/
655+
public String generateSecuredApiKey(@Nonnull String parentApiKey, @Nonnull SecuredAPIKeyRestrictions restrictions) throws Exception {
656+
Map<String, String> restrictionsMap = new HashMap<>();
657+
if (restrictions.getFilters() != null) restrictionsMap.put("filters", StringUtils.paramToString(restrictions.getFilters()));
658+
if (restrictions.getValidUntil() != 0) restrictionsMap.put("validUntil", StringUtils.paramToString(restrictions.getValidUntil()));
659+
if (restrictions.getRestrictIndices() != null) restrictionsMap.put(
660+
"restrictIndices",
661+
StringUtils.paramToString(restrictions.getRestrictIndices())
662+
);
663+
if (restrictions.getRestrictSources() != null) restrictionsMap.put(
664+
"restrictSources",
665+
StringUtils.paramToString(restrictions.getRestrictSources())
666+
);
667+
if (restrictions.getUserToken() != null) restrictionsMap.put("userToken", StringUtils.paramToString(restrictions.getUserToken()));
668+
669+
if (restrictions.getSearchParams() != null) {
670+
Map<String, Object> searchParamsMap = JsonSerializer
671+
.getObjectMapper()
672+
.convertValue(restrictions.getSearchParams(), new TypeReference<Map<String, Object>>() {});
673+
searchParamsMap.forEach((key, value) -> restrictionsMap.put(key, StringUtils.paramToString(value)));
674+
}
675+
676+
String queryStr = restrictionsMap
677+
.entrySet()
678+
.stream()
679+
.sorted(Map.Entry.comparingByKey())
680+
.map(entry -> String.format("%s=%s", entry.getKey(), entry.getValue()))
681+
.collect(Collectors.joining("&"));
682+
683+
String key = hmac(parentApiKey, queryStr);
684+
685+
return new String(Base64.getEncoder().encode(String.format("%s%s", key, queryStr).getBytes(Charset.forName("UTF8"))));
686+
}
687+
688+
private String hmac(String key, String msg) throws NoSuchAlgorithmException, InvalidKeyException {
689+
Mac hmac = Mac.getInstance("HmacSHA256");
690+
hmac.init(new SecretKeySpec(key.getBytes(), "HmacSHA256"));
691+
byte[] rawHmac = hmac.doFinal(msg.getBytes());
692+
StringBuilder sb = new StringBuilder(rawHmac.length * 2);
693+
for (byte b : rawHmac) {
694+
sb.append(String.format("%02x", b & 0xff));
695+
}
696+
return sb.toString();
697+
}
698+
699+
/**
700+
* Helper: Retrieves the remaining validity of the previous generated `secured_api_key`, the
701+
* `validUntil` parameter must have been provided.
702+
*
703+
* @param securedApiKey The secured API Key to check
704+
* @throws AlgoliaRuntimeException if <code>securedApiKey</code> is null, empty or whitespaces.
705+
* @throws AlgoliaRuntimeException if <code>securedApiKey</code> doesn't have a <code>validUntil
706+
* </code> parameter.
707+
*/
708+
public Duration getSecuredApiKeyRemainingValidity(@Nonnull String securedApiKey) {
709+
if (securedApiKey == null || securedApiKey.trim().isEmpty()) {
710+
throw new AlgoliaRuntimeException("securedAPIKey must not be empty, null or whitespaces");
711+
}
712+
713+
byte[] decodedBytes = Base64.getDecoder().decode(securedApiKey);
714+
String decodedString = new String(decodedBytes);
715+
716+
Pattern pattern = Pattern.compile("validUntil=\\d+");
717+
Matcher matcher = pattern.matcher(decodedString);
718+
719+
if (!matcher.find()) {
720+
throw new AlgoliaRuntimeException("The Secured API Key doesn't have a validUntil parameter.");
721+
}
722+
723+
String validUntilMatch = matcher.group(0);
724+
long timeStamp = Long.parseLong(validUntilMatch.replace("validUntil=", ""));
725+
726+
return Duration.ofSeconds(timeStamp - Instant.now().getEpochSecond());
727+
}
643728
{{/isSearchClient}}

0 commit comments

Comments
 (0)