Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement reload cache clearing for AuthenticationProvider, EnsoSecretReader and AuditLog #12541

Draft
wants to merge 22 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,9 @@
- [Added `Table.geo_distance` to calculate the distance between two
points.][12393]
- [The reload button clears the Enso Cloud request cache.][12526]
- [The reload button clears the AuthenticationProvider, EnsoSecretReader and
AuditLog caches.][12541]


[11235]: https://github.com/enso-org/enso/pull/11235
[11255]: https://github.com/enso-org/enso/pull/11255
Expand All @@ -304,6 +307,7 @@
[12017]: https://github.com/enso-org/enso/pull/12017
[12393]: https://github.com/enso-org/enso/pull/12393
[12526]: https://github.com/enso-org/enso/pull/12526
[12541]: https://github.com/enso-org/enso/pull/12526

#### Enso Language & Runtime

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,13 @@ polyglot java import org.enso.base.enso_cloud.AuthenticationProvider
and a new one will be returned. Because of that, this method may make network
requests.
get_access_token : Text
get_access_token = AuthenticationProvider.getAccessToken
get_access_token = AuthenticationProvider.INSTANCE.getAccessToken

## PRIVATE
Forcibly refreshes the access token.
refresh_access_token : Nothing
refresh_access_token =
AuthenticationProvider.getAuthenticationServiceEnsoInstance.force_refresh
AuthenticationProvider.INSTANCE.getAuthenticationServiceEnsoInstance.force_refresh

## PRIVATE
credentials_file : File
Expand Down
Original file line number Diff line number Diff line change
@@ -1,53 +1,80 @@
package org.enso.base.enso_cloud;

import org.enso.base.cache.ReloadDetector;
import org.enso.base.polyglot.EnsoMeta;
import org.graalvm.polyglot.Value;

public class AuthenticationProvider {
public class AuthenticationProvider implements ReloadDetector.HasClearableCache {
public static AuthenticationProvider INSTANCE = new AuthenticationProvider();

private AuthenticationProvider() {
ReloadDetector.register(this);
}

public interface AuthenticationService {
String get_access_token();

void force_refresh();
}

private static Value authenticationServiceAsEnso = null;
private static AuthenticationService authenticationServiceAsJava = null;
private Value authenticationServiceAsEnso = null;
private AuthenticationService authenticationServiceAsJava = null;

public static void reset() {
public void reset() {
authenticationServiceAsEnso = null;
authenticationServiceAsJava = null;
}

private static Value createAuthenticationService() {
private Value createAuthenticationService() {
return EnsoMeta.callStaticModuleMethod(
"Standard.Base.Enso_Cloud.Internal.Authentication", "instantiate_authentication_service");
}

private static void ensureServicesSetup() {
private void ensureServicesSetup() {
var ensoInstance = createAuthenticationService();
var javaInstance = ensoInstance.as(AuthenticationService.class);
authenticationServiceAsEnso = ensoInstance;
authenticationServiceAsJava = javaInstance;
}

static AuthenticationService getAuthenticationService() {
AuthenticationService getAuthenticationService() {
if (authenticationServiceAsJava == null) {
ensureServicesSetup();
}

return authenticationServiceAsJava;
}

public static Value getAuthenticationServiceEnsoInstance() {
public Value getAuthenticationServiceEnsoInstance() {
ReloadDetector.clearOnReload(this);

if (authenticationServiceAsEnso == null) {
ensureServicesSetup();
}

return authenticationServiceAsEnso;
}

public static String getAccessToken() {
public String getAccessToken() {
ReloadDetector.clearOnReload(this);

return getAuthenticationService().get_access_token();
}

@Override /* HasClearableCache */
public void clearCache() {
reset();
}

/** Public for testing. */
public boolean isCachedTestOnly() {
return authenticationServiceAsEnso != null;
}

/** Public for testing. */
// This is necessary because there is no other way to trigger a reload cache
// clear without re-filling the cache.
public void clearOnReloadTestOnly() {
ReloadDetector.clearOnReload(this);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ public static String getCloudSessionId() {

public static void flushCloudCaches() {
CloudRequestCache.INSTANCE.clear();
AuthenticationProvider.reset();
EnsoSecretReader.flushCache();
AuthenticationProvider.INSTANCE.reset();
EnsoSecretReader.INSTANCE.flushCache();
AuditLog.resetCache();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import java.util.Comparator;
import java.util.List;
import java.util.Properties;
import org.enso.base.cache.ReloadDetector;
import org.enso.base.cache.ResponseTooLargeException;
import org.enso.base.net.URISchematic;
import org.enso.base.net.URIWithSecrets;
Expand Down Expand Up @@ -102,7 +103,7 @@ public static EnsoHttpResponse makeRequest(
}

public static void deleteSecretFromCache(String secretId) {
EnsoSecretReader.removeFromCache(secretId);
EnsoSecretReader.INSTANCE.removeFromCache(secretId);
}

private static class RequestMaker implements EnsoHTTPResponseCache.RequestMaker {
Expand Down Expand Up @@ -192,6 +193,16 @@ public static EnsoHTTPResponseCache getOrCreateCache() {
return cache;
}

/** Visible for testing */
public static int getEnsoSecretReaderCacheSize() {
return EnsoSecretReader.INSTANCE.getCacheSize();
}

/** Visible for testing */
public static void simulateEnsoSecretReaderReload() {
ReloadDetector.simulateReloadTestOnly(EnsoSecretReader.INSTANCE);
}

private static final Comparator<Pair<String, String>> headerNameComparator =
Comparator.comparing((Pair<String, String> pair) -> pair.getLeft())
.thenComparing(Comparator.comparing(pair -> pair.getRight()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,25 @@
import java.net.http.HttpResponse;
import java.util.HashMap;
import java.util.Map;
import org.enso.base.cache.ReloadDetector;

/** * Internal class to read secrets from the Enso Cloud. */
class EnsoSecretReader {
private static final Map<String, String> secrets = new HashMap<>();
class EnsoSecretReader implements ReloadDetector.HasClearableCache {
static final EnsoSecretReader INSTANCE = new EnsoSecretReader();

static void flushCache() {
private final Map<String, String> secrets = new HashMap<>();

private EnsoSecretReader() {
ReloadDetector.register(this);
}

void flushCache() {
secrets.clear();
}

static void removeFromCache(String secretId) {
void removeFromCache(String secretId) {
ReloadDetector.clearOnReloadIfRegistered(this);

secrets.remove(secretId);
}

Expand All @@ -26,21 +35,23 @@ static void removeFromCache(String secretId) {
* @param secretId the ID of the secret to read.
* @return the secret value.
*/
static String readSecret(String secretId) {
String readSecret(String secretId) {
ReloadDetector.clearOnReloadIfRegistered(this);

if (secrets.containsKey(secretId)) {
return secrets.get(secretId);
}

return fetchSecretValue(secretId, 3);
}

private static String fetchSecretValue(String secretId, int retryCount) {
private String fetchSecretValue(String secretId, int retryCount) {
var apiUri = CloudAPI.getAPIRootURI() + "s3cr3tz/" + secretId;
var client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).build();
var request =
HttpRequest.newBuilder()
.uri(URI.create(apiUri))
.header("Authorization", "Bearer " + AuthenticationProvider.getAccessToken())
.header("Authorization", "Bearer " + AuthenticationProvider.INSTANCE.getAccessToken())
.GET()
.build();

Expand All @@ -64,7 +75,7 @@ private static String fetchSecretValue(String secretId, int retryCount) {
"Unable to read secret - numerous " + kind + " failures (status code " + status + ").");
} else {
// We forcibly refresh the access token and try again.
AuthenticationProvider.getAuthenticationService().force_refresh();
AuthenticationProvider.INSTANCE.getAuthenticationService().force_refresh();
return fetchSecretValue(secretId, retryCount - 1);
}
}
Expand All @@ -80,9 +91,19 @@ private static String fetchSecretValue(String secretId, int retryCount) {
return secretValue;
}

private static String readValueFromString(String json) {
private String readValueFromString(String json) {
var base64 = json.substring(1, json.length() - 1).translateEscapes();
return new String(
java.util.Base64.getDecoder().decode(base64), java.nio.charset.StandardCharsets.UTF_8);
}

@Override /* HasClearableCache */
public void clearCache() {
flushCache();
}

/** Visible for testing */
public int getCacheSize() {
return secrets.size();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ protected static String resolveValue(HideableValue value) {
return switch (value) {
case HideableValue.PlainValue plainValue -> plainValue.value();
case HideableValue.SecretValue secretValue -> {
yield EnsoSecretReader.readSecret(secretValue.secretId());
yield EnsoSecretReader.INSTANCE.readSecret(secretValue.secretId());
}
case HideableValue.ConcatValues concatValues -> {
String left = resolveValue(concatValues.left());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import org.enso.base.cache.ReloadDetector;
import org.enso.base.enso_cloud.AuthenticationProvider;
import org.enso.base.enso_cloud.CloudAPI;

/**
* Gives access to the low-level log event API in the Cloud and manages asynchronously submitting
* the logs.
*/
class AuditLogApiAccess {
public final class AuditLogApiAccess implements ReloadDetector.HasClearableCache {
private static final Logger logger = Logger.getLogger(AuditLogApiAccess.class.getName());

/**
Expand All @@ -45,16 +46,21 @@ private AuditLogApiAccess() {
// If the thread is idle for 60 seconds, it will be shut down.
backgroundThreadService =
new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
ReloadDetector.register(this);
}

public Future<Void> logWithConfirmation(LogMessage message) {
ReloadDetector.clearOnReload(this);

var currentRequestConfig = getRequestConfig();
CompletableFuture<Void> completionNotification = new CompletableFuture<>();
enqueueJob(new LogJob(message, completionNotification, currentRequestConfig));
return completionNotification;
}

public void logWithoutConfirmation(LogMessage message) {
ReloadDetector.clearOnReload(this);

var currentRequestConfig = getRequestConfig();
enqueueJob(new LogJob(message, null, currentRequestConfig));
}
Expand Down Expand Up @@ -195,7 +201,7 @@ private RequestConfig getRequestConfig() {
}

var uri = URI.create(CloudAPI.getAPIRootURI() + "logs");
var config = new RequestConfig(uri, AuthenticationProvider.getAccessToken());
var config = new RequestConfig(uri, AuthenticationProvider.INSTANCE.getAccessToken());
cachedRequestConfig = config;
return config;
}
Expand Down Expand Up @@ -265,4 +271,21 @@ record LogJob(
void resetCache() {
cachedRequestConfig = null;
}

@Override /* HasClearableCache */
public void clearCache() {
resetCache();
}

/** Public for testing. */
public boolean isCachedTestOnly() {
return cachedRequestConfig != null;
}

/** Public for testing. */
// This is necessary because there is no other way to trigger a reload cache
// clear without re-filling the cache.
public void clearOnReloadTestOnly() {
ReloadDetector.clearOnReload(this);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,8 @@ public void clearCache() {
LOGGER.error("Unable to close " + record, e);
}
}
records.clear();
}
records.clear();
}

/** Public for testing. */
Expand Down
11 changes: 11 additions & 0 deletions test/Base_Tests/src/Network/Enso_Cloud/Audit_Log_Spec.enso
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import Standard.Test.Test_Environment

import project.Network.Enso_Cloud.Cloud_Tests_Setup.Cloud_Tests_Setup

polyglot java import org.enso.base.cache.ReloadDetector
polyglot java import org.enso.base.enso_cloud.logging.LogApiAccess

add_specs suite_builder =
## By default, these tests are run only on the Cloud mock, not on the real deployment.
Expand Down Expand Up @@ -65,6 +67,15 @@ add_specs suite_builder =
events = get_audit_log_events . filter ev-> (ev.metadata.get "my_field") == random_payload
events.length . should_equal 121

suite_builder.group "Clear cache on reload" group_builder->
group_builder.specify "LogApiAccess cache should be cleared when a reload is detected" <| setup.with_prepared_environment <|
Audit_Log.report_event "TestEvent" "Message"
LogApiAccess.INSTANCE.isCachedTestOnly . should_be_true
ReloadDetector.simulateReloadTestOnly LogApiAccess.INSTANCE
LogApiAccess.INSTANCE.clearOnReloadTestOnly . should_succeed # Triggers cache clearing
LogApiAccess.INSTANCE.isCachedTestOnly . should_be_false


main filter=Nothing =
suite = Test.build suite_builder->
add_specs suite_builder
Expand Down
Loading
Loading