Skip to content

Commit

Permalink
feat(clients): helper to switch API key in use (#3616)
Browse files Browse the repository at this point in the history
Co-authored-by: Pierre Millot <pierre.millot@algolia.com>
  • Loading branch information
Fluf22 and millotp authored Sep 6, 2024
1 parent a4e3eec commit 2c65a8d
Show file tree
Hide file tree
Showing 88 changed files with 567 additions and 131 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ protected AlgoliaConfig(string appId, string apiKey, string clientName, string c
/// The admin API Key
/// </summary>
/// <returns></returns>
public string ApiKey { get; }
public string ApiKey { get; set; }

/// <summary>
/// Configurations hosts
Expand Down Expand Up @@ -97,5 +97,16 @@ internal Dictionary<string, string> BuildHeaders()
DefaultHeaders[Defaults.UserAgentHeader.ToLowerInvariant()] = UserAgent.ToString();
return DefaultHeaders;
}

/// <summary>
/// Helper to switch the API key sent with each request
/// </summary>
/// <param name="apiKey">Your API Key</param>
/// <returns></returns>
public void SetClientApiKey(string apiKey)
{
ApiKey = apiKey;
DefaultHeaders[Defaults.AlgoliaApiKeyHeader.ToLowerInvariant()] = apiKey;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ internal class HttpTransport
private readonly IHttpRequester _httpClient;
private readonly ISerializer _serializer;
private readonly RetryStrategy _retryStrategy;
private readonly AlgoliaConfig _algoliaConfig;
internal AlgoliaConfig _algoliaConfig;
private string _errorMessage;
private readonly ILogger<HttpTransport> _logger;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@ import 'package:algolia_client_core/src/config/client_options.dart';

/// An abstract class representing an API client with specific properties and options.
abstract interface class ApiClient {
/// The unique identifier for the application using the API client.
String get appId;

/// The API key used for authentication.
String get apiKey;

/// A set of custom client options to configure the behavior of the API client.
ClientOptions get options;

/// Allow switching the API key used to authenticate requests.
void setClientApiKey({required String apiKey});

/// Dispose of underlying resources.
void dispose();
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class AuthInterceptor extends Interceptor {
final String appId;

/// The API key used for Algolia authentication.
final String apiKey;
String apiKey;

/// Constructs an [AuthInterceptor] with the provided application id and API key.
AuthInterceptor({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import 'package:dio/dio.dart';
/// response conversion and error handling.
class DioRequester implements Requester {
/// The underlying Dio client.
final Dio _client;
final AuthInterceptor _authInterceptor;
late final Dio _client;

/// Constructs a [DioRequester] with the given [appId], [apiKey], and [options].
DioRequester({
Expand All @@ -28,29 +29,30 @@ class DioRequester implements Requester {
Function(Object?)? logger,
Iterable<Interceptor>? interceptors,
HttpClientAdapter? httpClientAdapter,
}) : _client = Dio(
BaseOptions(
headers: headers,
connectTimeout: connectTimeout,
}) : _authInterceptor = AuthInterceptor(
appId: appId,
apiKey: apiKey,
) {
_client = Dio(
BaseOptions(
headers: headers,
connectTimeout: connectTimeout,
),
)..interceptors.addAll([
_authInterceptor,
AgentInterceptor(
agent: AlgoliaAgent(packageVersion)
..addAll(clientSegments ?? const [])
..addAll(Platform.agentSegments()),
),
if (logger != null)
LogInterceptor(
requestBody: true,
responseBody: true,
logPrint: logger,
),
)..interceptors.addAll([
AuthInterceptor(
appId: appId,
apiKey: apiKey,
),
AgentInterceptor(
agent: AlgoliaAgent(packageVersion)
..addAll(clientSegments ?? const [])
..addAll(Platform.agentSegments()),
),
if (logger != null)
LogInterceptor(
requestBody: true,
responseBody: true,
logPrint: logger,
),
if (interceptors != null) ...interceptors,
]) {
if (interceptors != null) ...interceptors,
]);
if (httpClientAdapter != null) {
_client.httpClientAdapter = httpClientAdapter;
}
Expand Down Expand Up @@ -114,4 +116,9 @@ class DioRequester implements Requester {

@override
void close() => _client.close();

@override
void setClientApiKey(String apiKey) {
_authInterceptor.apiKey = apiKey;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ abstract class Requester {
/// The method returns a Future that resolves to an [HttpResponse].
Future<HttpResponse> perform(HttpRequest request);

/// Allows to switch the API key used to authenticate requests.
void setClientApiKey(String apiKey);

/// Closes any underlying resources that the Requester might be using.
///
/// By default, it does nothing (no-op), but it can be implemented to handle resource cleanup
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;
import org.jetbrains.annotations.Nullable;
import javax.annotation.Nonnull;

/**
* Represents a base client for making API requests. The client uses a {@link Requester} for
Expand All @@ -26,6 +27,7 @@ public abstract class ApiClient implements Closeable {

private final Requester requester;
private final ExecutorService executor;
private AuthInterceptor authInterceptor;

/** Constructs a new instance of the {@link ApiClient}. */
protected ApiClient(String appId, String apiKey, String clientName, @Nullable ClientOptions options, List<Host> defaultHosts) {
Expand All @@ -52,8 +54,9 @@ private Requester defaultRequester(String appId, String apiKey, String clientNam
List<StatefulHost> statefulHosts = hosts.stream().map(StatefulHost::new).collect(Collectors.toList());

JsonSerializer serializer = JsonSerializer.builder().setCustomConfig(options.getMapperConfig()).build();
this.authInterceptor = new AuthInterceptor(appId, apiKey);
HttpRequester.Builder builder = new HttpRequester.Builder(serializer)
.addInterceptor(new AuthInterceptor(appId, apiKey))
.addInterceptor(authInterceptor)
.addInterceptor(new UserAgentInterceptor(algoliaAgent))
.addInterceptor(new RetryStrategy(statefulHosts));
if (options.getRequesterConfig() != null) {
Expand All @@ -62,6 +65,15 @@ private Requester defaultRequester(String appId, String apiKey, String clientNam
return builder.build(options);
}

/**
* Helper method to switch the API key used to authenticate the requests.
*
* @param apiKey The new API key to be used from now on.
*/
public void setClientApiKey(@Nonnull String apiKey) {
this.authInterceptor.setApiKey(apiKey);
}

/**
* Executes an HTTP request asynchronously and returns a {@link CompletableFuture} of the response
* deserialized into a specified type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@ public final class AuthInterceptor implements Interceptor {
private static final String HEADER_API_KEY = "x-algolia-api-key";

private final String applicationId;
private final String apiKey;
private String apiKey;

public AuthInterceptor(String applicationId, String apiKey) {
this.applicationId = applicationId;
this.apiKey = apiKey;
}

public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}

@Nonnull
@Override
public Response intercept(Chain chain) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*

private const val HEADER_APPLICATION_ID = "x-algolia-application-id"
private const val HEADER_APIKEY = "x-algolia-api-key"
public const val HEADER_APIKEY: String = "x-algolia-api-key"

internal fun algoliaHttpClient(
appId: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,6 @@ public interface Requester {
requestOptions: RequestOptions? = null,
returnType: TypeInfo,
): T

public fun setClientApiKey(apiKey: String);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.algolia.client.transport.internal

import com.algolia.client.configuration.CallType
import com.algolia.client.configuration.Host
import com.algolia.client.configuration.internal.HEADER_APIKEY
import com.algolia.client.exception.AlgoliaRetryException
import com.algolia.client.exception.internal.asApiException
import com.algolia.client.exception.internal.asClientException
Expand Down Expand Up @@ -38,6 +39,15 @@ public class KtorRequester(
private val mutex: Mutex = Mutex()
private val retryableHosts = hosts.map { RetryableHost(it) }

public override fun setClientApiKey(apiKey: String) {
headers {
if (contains(HEADER_APIKEY)) {
remove(HEADER_APIKEY)
}
append(HEADER_APIKEY, apiKey)
}
}

override suspend fun <T> execute(
requestConfig: RequestConfig,
requestOptions: RequestOptions?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public function __construct(array $config = [])
throw new AlgoliaException('`apiKey` is missing.');
}

$this->setAlgoliaApiKey($config['apiKey']);
$this->setClientApiKey($config['apiKey']);
$this->setAuth('x-algolia-api-key', $config['apiKey']);

$this->setAppId($config['appId']);
Expand Down Expand Up @@ -193,13 +193,6 @@ public function getAlgoliaApiKey()
return $this->config['apiKey'];
}

public function setAlgoliaApiKey($apiKey)
{
$this->config['apiKey'] = $apiKey;

return $this;
}

public function getHosts()
{
return $this->config['hosts'];
Expand Down Expand Up @@ -272,6 +265,30 @@ public function setDefaultHeaders(array $defaultHeaders)
return $this;
}

/**
* Switch the API key used to authenticate requessts.
*
* @param string $apiKey The new API key to be used
*
* @return void
*/
public function setClientApiKey($apiKey)
{
$this->config['apiKey'] = $apiKey;

return $this;
}

/**
* @deprecated This method is deprecated. Use setClientApiKey() instead.
*
* @param mixed $apiKey
*/
public function setAlgoliaApiKey($apiKey)
{
return $this->setClientApiKey($apiKey);
}

/**
* Sets the user agent of the api client.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,8 @@ def __init__(self, app_id: Optional[str] = None, api_key: Optional[str] = None):
self.headers = None
self.proxies = None
self.hosts = None

def set_client_api_key(self, api_key: str) -> None:
"""Sets a new API key to authenticate requests."""
self.api_key = api_key
self.headers["x-algolia-api-key"] = api_key
4 changes: 4 additions & 0 deletions clients/algoliasearch-client-ruby/lib/algolia/api_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ def self.default
@@default ||= ApiClient.new
end

def set_client_api_key(api_key)
@config.set_client_api_key(api_key)
end

# Call an API with given options.
#
# @return [Http::Response] the response.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ def initialize(app_id, api_key, hosts, client_name, opts = {})
yield(self) if block_given?
end

def set_client_api_key(api_key)
@api_key = api_key
@header_params["X-Algolia-API-Key"] = api_key
end

# The default Configuration object.
def self.default
@@default ||= Configuration.new
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ abstract class ApiClient(
throw AlgoliaClientException("`apiKey` is missing.")
}

private val authInterceptor = new AuthInterceptor(appId, apiKey)

private val requester = options.customRequester match {
case Some(customRequester) => customRequester
case None =>
Expand All @@ -62,7 +64,7 @@ abstract class ApiClient(

val builder = HttpRequester
.builder(options.customFormats.getOrElse(formats))
.withInterceptor(new AuthInterceptor(appId, apiKey))
.withInterceptor(authInterceptor)
.withInterceptor(new UserAgentInterceptor(algoliaAgent))
.withInterceptor(new RetryStrategy(statefulHosts))

Expand All @@ -89,4 +91,8 @@ abstract class ApiClient(
override def close(): Unit = {
Try(requester.close())
}

def setClientApiKey(apiKey: String): Unit = {
authInterceptor.setApiKey(apiKey)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ import okhttp3.{Interceptor, Response}
* API key
*/
private[algoliasearch] class AuthInterceptor(
applicationId: String,
apiKey: String
applicationId: String,
private var apiKey: String
) extends Interceptor {

private val HeaderApplicationId = "x-algolia-application-id"
private val HeaderApiKey = "x-algolia-api-key"

def setApiKey(newApiKey: String): Unit = {
apiKey = newApiKey
}

override def intercept(chain: Interceptor.Chain): Response = {
val originalRequest = chain.request()
val builder = originalRequest.newBuilder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public protocol BaseConfiguration {
var hosts: [RetryableHost] { get }

/// Default headers that should be applied to every request.
var defaultHeaders: [String: String]? { get }
var defaultHeaders: [String: String]? { get set }

/// Compression type
var compression: CompressionAlgorithm { get }
Expand All @@ -48,7 +48,7 @@ public struct DefaultConfiguration: BaseConfiguration {
public let writeTimeout: TimeInterval = 30
public let readTimeout: TimeInterval = 5
public let logLevel: LogLevel = .info
public let defaultHeaders: [String: String]? = [:]
public var defaultHeaders: [String: String]? = [:]
public var hosts: [RetryableHost] = []
public let compression: CompressionAlgorithm = .none
}
Loading

0 comments on commit 2c65a8d

Please sign in to comment.