diff --git a/conf/default-config.json b/conf/default-config.json index 224df8906..932f8dbbd 100644 --- a/conf/default-config.json +++ b/conf/default-config.json @@ -35,6 +35,7 @@ "enclave_platform": null, "failure_shutdown_wait_hours": 120, "sharing_token_expiry_seconds": 2592000, - "operator_type": "public" - + "operator_type": "public", + "core_config_path": "/operator/config", + "config_scan_period_ms": 300000 } diff --git a/conf/integ-config.json b/conf/integ-config.json index f1cf90742..4aa307c35 100644 --- a/conf/integ-config.json +++ b/conf/integ-config.json @@ -14,6 +14,6 @@ "optout_api_token": "test-operator-key", "optout_api_uri": "http://localhost:8081/optout/replicate", "salts_expired_shutdown_hours": 12, - "operator_type": "public" - + "operator_type": "public", + "core_operator_config_path": "http://localhost:8088/operator/config" } \ No newline at end of file diff --git a/conf/local-config.json b/conf/local-config.json index 6a357dba1..c73940613 100644 --- a/conf/local-config.json +++ b/conf/local-config.json @@ -36,5 +36,6 @@ "key_sharing_endpoint_provide_app_names": true, "client_side_token_generate_log_invalid_http_origins": true, "salts_expired_shutdown_hours": 12, - "operator_type": "public" + "operator_type": "public", + "remote_config_feature_flag": true } diff --git a/scripts/aws/conf/integ-uid2-config.json b/scripts/aws/conf/integ-uid2-config.json index a7272a26a..91b53b1f2 100644 --- a/scripts/aws/conf/integ-uid2-config.json +++ b/scripts/aws/conf/integ-uid2-config.json @@ -11,5 +11,6 @@ "core_attest_url": "https://core-integ.uidapi.com/attest", "optout_api_uri": "https://optout-integ.uidapi.com/optout/replicate", "optout_s3_folder": "uid-optout-integ/", - "allow_legacy_api": false + "allow_legacy_api": false, + "core_operator_config_path": "https://core-integ.uidapi.com/operator/config" } diff --git a/src/main/java/com/uid2/operator/Const.java b/src/main/java/com/uid2/operator/Const.java index 4d32b9034..b1623f5ee 100644 --- a/src/main/java/com/uid2/operator/Const.java +++ b/src/main/java/com/uid2/operator/Const.java @@ -29,5 +29,12 @@ public class Config extends com.uid2.shared.Const.Config { public static final String OptOutStatusMaxRequestSize = "optout_status_max_request_size"; public static final String MaxInvalidPaths = "logging_limit_max_invalid_paths_per_interval"; public static final String MaxVersionBucketsPerSite = "logging_limit_max_version_buckets_per_site"; + + public static final String CoreConfigPath = "core_operator_config_path"; + public static final String ConfigScanPeriodMs = "config_scan_period_ms"; + public static final String Config = "config"; + public static final String identityV3 = "identity_v3"; + public static final String RemoteConfigFeatureFlag = "remote_config_feature_flag"; + public static final String RemoteConfigFlagConfigMapPath = "remote_config_feature_flag_path"; } } diff --git a/src/main/java/com/uid2/operator/Main.java b/src/main/java/com/uid2/operator/Main.java index a307e3695..470fafd80 100644 --- a/src/main/java/com/uid2/operator/Main.java +++ b/src/main/java/com/uid2/operator/Main.java @@ -8,8 +8,7 @@ import com.uid2.operator.monitoring.IStatsCollectorQueue; import com.uid2.operator.monitoring.OperatorMetrics; import com.uid2.operator.monitoring.StatsCollectorVerticle; -import com.uid2.operator.service.SecureLinkValidatorService; -import com.uid2.operator.service.ShutdownService; +import com.uid2.operator.service.*; import com.uid2.operator.vertx.Endpoints; import com.uid2.operator.vertx.OperatorShutdownHandler; import com.uid2.operator.store.CloudSyncOptOutStore; @@ -37,6 +36,7 @@ import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; import io.micrometer.prometheus.PrometheusMeterRegistry; import io.micrometer.prometheus.PrometheusRenameFilter; +import io.vertx.config.ConfigRetriever; import io.vertx.core.*; import io.vertx.core.http.HttpServerOptions; import io.vertx.core.http.impl.HttpUtils; @@ -203,7 +203,9 @@ else if (!Utils.isProductionEnvironment()) { } Vertx vertx = createVertx(); - VertxUtils.createConfigRetriever(vertx).getConfig(ar -> { + ConfigRetriever configRetriever = VertxUtils.createConfigRetriever(vertx); + + configRetriever.getConfig(ar -> { if (ar.failed()) { LOGGER.error("Unable to read config: " + ar.cause().getMessage(), ar.cause()); return; @@ -267,39 +269,81 @@ private ICloudStorage wrapCloudStorageForOptOut(ICloudStorage cloudStorage) { private void run() throws Exception { this.createVertxInstancesMetric(); this.createVertxEventLoopsMetric(); - Supplier operatorVerticleSupplier = () -> { - UIDOperatorVerticle verticle = new UIDOperatorVerticle(config, this.clientSideTokenGenerate, siteProvider, clientKeyProvider, clientSideKeypairProvider, getKeyManager(), saltProvider, optOutStore, Clock.systemUTC(), _statsCollectorQueue, new SecureLinkValidatorService(this.serviceLinkProvider, this.serviceProvider), this.shutdownHandler::handleSaltRetrievalResponse); - return verticle; - }; - - DeploymentOptions options = new DeploymentOptions(); - int svcInstances = this.config.getInteger(Const.Config.ServiceInstancesProp); - options.setInstances(svcInstances); - Promise compositePromise = Promise.promise(); - List fs = new ArrayList<>(); - fs.add(createAndDeployStatsCollector()); - fs.add(createStoreVerticles()); - - CompositeFuture.all(fs).onComplete(ar -> { - if (ar.failed()) compositePromise.fail(new Exception(ar.cause())); - else compositePromise.complete(); - }); + ConfigRetrieverFactory configRetrieverFactory = new ConfigRetrieverFactory(); + ConfigRetriever dynamicConfigRetriever = configRetrieverFactory.createRemoteConfigRetriever(vertx, config, this.createOperatorKeyRetriever().retrieve()); + ConfigRetriever staticConfigRetriever = configRetrieverFactory.createJsonRetriever(vertx, config); + + Future dynamicConfigFuture = ConfigService.create(dynamicConfigRetriever); + Future staticConfigFuture = ConfigService.create(staticConfigRetriever); + + ConfigRetriever featureFlagConfigRetriever = configRetrieverFactory.createFileRetriever( + vertx, + config.getString(Const.Config.RemoteConfigFlagConfigMapPath, "conf/local-config.json") + ); + + Future featureFlagFuture = featureFlagConfigRetriever.getConfig(); + + Future.all(dynamicConfigFuture, staticConfigFuture, featureFlagFuture) + .compose(configServiceManagerCompositeFuture -> { + ConfigService dynamicConfigService = configServiceManagerCompositeFuture.resultAt(0); + ConfigService staticConfigService = configServiceManagerCompositeFuture.resultAt(1); + JsonObject featureFlagConfig = configServiceManagerCompositeFuture.resultAt(2); + + boolean featureFlag = featureFlagConfig.getBoolean(Const.Config.RemoteConfigFeatureFlag, true); + + ConfigServiceManager configServiceManager = new ConfigServiceManager(vertx, dynamicConfigService, staticConfigService, featureFlag); + + featureFlagConfigRetriever.listen(change -> { + JsonObject newConfig = change.getNewConfiguration(); + boolean useDynamicConfig = newConfig.getBoolean(Const.Config.RemoteConfigFeatureFlag, true); + configServiceManager.updateConfigService(useDynamicConfig).onComplete(update -> { + if (update.succeeded()) { + LOGGER.info("Remote config feature flag toggled successfully"); + } else { + LOGGER.error("Failed to toggle remote config feature flag: " + update.cause()); + } + }); + }); + + IConfigService configService = configServiceManager.getDelegatingConfigService(); + Supplier operatorVerticleSupplier = () -> { + UIDOperatorVerticle verticle = new UIDOperatorVerticle(configService, config, this.clientSideTokenGenerate, siteProvider, clientKeyProvider, clientSideKeypairProvider, getKeyManager(), saltProvider, optOutStore, Clock.systemUTC(), _statsCollectorQueue, new SecureLinkValidatorService(this.serviceLinkProvider, this.serviceProvider), this.shutdownHandler::handleSaltRetrievalResponse); + return verticle; + }; + + DeploymentOptions options = new DeploymentOptions(); + int svcInstances = this.config.getInteger(Const.Config.ServiceInstancesProp); + options.setInstances(svcInstances); + + Promise compositePromise = Promise.promise(); + List fs = new ArrayList<>(); + fs.add(createAndDeployStatsCollector()); + try { + fs.add(createStoreVerticles()); + } catch (Exception e) { + throw new RuntimeException(e); + } - compositePromise.future() - .compose(v -> { - metrics.setup(); - vertx.setPeriodic(60000, id -> metrics.update()); - - Promise promise = Promise.promise(); - vertx.deployVerticle(operatorVerticleSupplier, options, promise); - return promise.future(); - }) - .onFailure(t -> { - LOGGER.error("Failed to bootstrap operator: " + t.getMessage(), new Exception(t)); - vertx.close(); - System.exit(1); - }); + CompositeFuture.all(fs).onComplete(ar -> { + if (ar.failed()) compositePromise.fail(new Exception(ar.cause())); + else compositePromise.complete(); + }); + + return compositePromise.future() + .compose(v -> { + metrics.setup(); + vertx.setPeriodic(60000, id -> metrics.update()); + Promise promise = Promise.promise(); + vertx.deployVerticle(operatorVerticleSupplier, options, promise); + return promise.future(); + }) + .onFailure(t -> { + LOGGER.error("Failed to bootstrap operator: " + t.getMessage(), new Exception(t)); + vertx.close(); + System.exit(1); + }); + }); } private Future createStoreVerticles() throws Exception { diff --git a/src/main/java/com/uid2/operator/service/ConfigRetrieverFactory.java b/src/main/java/com/uid2/operator/service/ConfigRetrieverFactory.java new file mode 100644 index 000000000..488c9d69e --- /dev/null +++ b/src/main/java/com/uid2/operator/service/ConfigRetrieverFactory.java @@ -0,0 +1,62 @@ +package com.uid2.operator.service; + +import io.vertx.config.ConfigRetriever; +import io.vertx.config.ConfigRetrieverOptions; +import io.vertx.config.ConfigStoreOptions; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; + +import java.net.URI; +import java.net.URISyntaxException; + +import static com.uid2.operator.Const.Config.ConfigScanPeriodMs; +import static com.uid2.operator.Const.Config.CoreConfigPath; + +public class ConfigRetrieverFactory { + public ConfigRetriever createRemoteConfigRetriever(Vertx vertx, JsonObject bootstrapConfig, String operatorKey) throws URISyntaxException { + String configPath = bootstrapConfig.getString(CoreConfigPath); + URI uri = new URI(configPath); + + ConfigStoreOptions httpStore = new ConfigStoreOptions() + .setType("http") + .setOptional(true) + .setConfig(new JsonObject() + .put("host", uri.getHost()) + .put("port", uri.getPort()) + .put("path", uri.getPath()) + .put("headers", new JsonObject() + .put("Authorization", "Bearer " + operatorKey))); + + ConfigRetrieverOptions retrieverOptions = new ConfigRetrieverOptions() + .setScanPeriod(bootstrapConfig.getLong(ConfigScanPeriodMs)) + .addStore(httpStore); + + return ConfigRetriever.create(vertx, retrieverOptions); + } + + public ConfigRetriever createJsonRetriever(Vertx vertx, JsonObject config) { + ConfigStoreOptions jsonStore = new ConfigStoreOptions() + .setType("json") + .setConfig(config); + + ConfigRetrieverOptions retrieverOptions = new ConfigRetrieverOptions() + .setScanPeriod(-1) + .addStore(jsonStore); + + + return ConfigRetriever.create(vertx, retrieverOptions); + } + + public ConfigRetriever createFileRetriever(Vertx vertx, String path) { + ConfigStoreOptions fileStore = new ConfigStoreOptions() + .setType("file") + .setConfig(new JsonObject() + .put("path", path) + .put("format", "json")); + + ConfigRetrieverOptions retrieverOptions = new ConfigRetrieverOptions() + .addStore(fileStore); + + return ConfigRetriever.create(vertx, retrieverOptions); + } +} diff --git a/src/main/java/com/uid2/operator/service/ConfigService.java b/src/main/java/com/uid2/operator/service/ConfigService.java new file mode 100644 index 000000000..cd5b8b9bc --- /dev/null +++ b/src/main/java/com/uid2/operator/service/ConfigService.java @@ -0,0 +1,70 @@ +package com.uid2.operator.service; + +import com.uid2.operator.Const; +import io.vertx.config.ConfigRetriever; +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.json.JsonObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.uid2.operator.service.ConfigValidatorUtil.*; +import static com.uid2.operator.service.UIDOperatorService.*; + +public class ConfigService implements IConfigService { + + private final ConfigRetriever configRetriever; + private static final Logger logger = LoggerFactory.getLogger(ConfigService.class); + + private ConfigService(ConfigRetriever configRetriever) { + this.configRetriever = configRetriever; + this.configRetriever.setConfigurationProcessor(this::configValidationHandler); + } + + public static Future create(ConfigRetriever configRetriever) { + Promise promise = Promise.promise(); + + ConfigService instance = new ConfigService(configRetriever); + + configRetriever.getConfig(ar -> { + if (ar.succeeded()) { + System.out.println("Successfully loaded config"); + promise.complete(instance); + } else { + System.err.println("Failed to load config: " + ar.cause().getMessage()); + logger.error("Failed to load config: {}", ar.cause().getMessage()); + promise.fail(ar.cause()); + } + }); + + return promise.future(); + } + + @Override + public JsonObject getConfig() { + return configRetriever.getCachedConfig(); + } + + private JsonObject configValidationHandler(JsonObject config) { + boolean isValid = true; + Integer identityExpiresAfter = config.getInteger(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS); + Integer refreshExpiresAfter = config.getInteger(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS); + Integer refreshIdentityAfter = config.getInteger(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS); + Integer maxBidstreamLifetimeSeconds = config.getInteger(Const.Config.MaxBidstreamLifetimeSecondsProp, identityExpiresAfter); + + isValid &= validateIdentityRefreshTokens(identityExpiresAfter, refreshExpiresAfter, refreshIdentityAfter); + + isValid &= validateBidstreamLifetime(maxBidstreamLifetimeSeconds, identityExpiresAfter); + + if (!isValid) { + logger.error("Failed to update config"); + JsonObject lastConfig = this.getConfig(); + if (lastConfig == null || lastConfig.isEmpty()) { + throw new RuntimeException("Invalid config retrieved and no previous config to revert to"); + } + return lastConfig; + } + + return config; + } +} diff --git a/src/main/java/com/uid2/operator/service/ConfigServiceManager.java b/src/main/java/com/uid2/operator/service/ConfigServiceManager.java new file mode 100644 index 000000000..5fa3ffac2 --- /dev/null +++ b/src/main/java/com/uid2/operator/service/ConfigServiceManager.java @@ -0,0 +1,56 @@ +package com.uid2.operator.service; + +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import io.vertx.core.shareddata.Lock; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ConfigServiceManager { + private final Vertx vertx; + private final DelegatingConfigService delegatingConfigService; + private final IConfigService dynamicConfigService; + private final IConfigService staticConfigService; + private static final Logger logger = LoggerFactory.getLogger(ConfigServiceManager.class); + + public ConfigServiceManager(Vertx vertx, IConfigService dynamicConfigService, IConfigService staticConfigService, boolean useDynamicConfig) { + this.vertx = vertx; + this.dynamicConfigService = dynamicConfigService; + this.staticConfigService = staticConfigService; + this.delegatingConfigService = new DelegatingConfigService(useDynamicConfig ? dynamicConfigService : staticConfigService); + } + + public Future updateConfigService(boolean useDynamicConfig) { + Promise promise = Promise.promise(); + vertx.sharedData().getLocalLock("updateConfigServiceLock", lockAsyncResult -> { + if (lockAsyncResult.succeeded()) { + Lock lock = lockAsyncResult.result(); + try { + if (useDynamicConfig) { + logger.info("Switching to DynamicConfigService"); + delegatingConfigService.updateConfigService(dynamicConfigService); + } else { + logger.info("Switching to StaticConfigService"); + delegatingConfigService.updateConfigService(staticConfigService); + } + promise.complete(); + } catch (Exception e) { + promise.fail(e); + } finally { + lock.release(); + } + } else { + logger.error("Failed to acquire lock for updating active ConfigService", lockAsyncResult.cause()); + promise.fail(lockAsyncResult.cause()); + } + }); + + return promise.future(); + } + + public IConfigService getDelegatingConfigService() { + return delegatingConfigService; + } + +} diff --git a/src/main/java/com/uid2/operator/service/ConfigValidatorUtil.java b/src/main/java/com/uid2/operator/service/ConfigValidatorUtil.java new file mode 100644 index 000000000..c7643d707 --- /dev/null +++ b/src/main/java/com/uid2/operator/service/ConfigValidatorUtil.java @@ -0,0 +1,47 @@ +package com.uid2.operator.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.uid2.operator.service.UIDOperatorService.*; + +public class ConfigValidatorUtil { + private static final Logger logger = LoggerFactory.getLogger(ConfigValidatorUtil.class); + public static final String VALUES_ARE_NULL = "Required config values are null"; + + public static Boolean validateIdentityRefreshTokens(Integer identityExpiresAfter, Integer refreshExpiresAfter, Integer refreshIdentityAfter) { + boolean isValid = true; + + if (identityExpiresAfter == null || refreshExpiresAfter == null || refreshIdentityAfter == null) { + logger.error(VALUES_ARE_NULL); + return false; + } + + + if (identityExpiresAfter > refreshExpiresAfter) { + logger.error(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS + " must be >= " + IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS); + isValid = false; + } + if (refreshIdentityAfter > identityExpiresAfter) { + logger.error(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS + " must be >= " + REFRESH_IDENTITY_TOKEN_AFTER_SECONDS); + isValid = false; + } + if (refreshIdentityAfter > refreshExpiresAfter) { + logger.error(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS + " must be >= " + REFRESH_IDENTITY_TOKEN_AFTER_SECONDS); + } + return isValid; + } + + public static Boolean validateBidstreamLifetime(Integer maxBidstreamLifetimeSeconds, Integer identityTokenExpiresAfterSeconds) { + if (maxBidstreamLifetimeSeconds == null || identityTokenExpiresAfterSeconds == null) { + logger.error(VALUES_ARE_NULL); + return false; + } + + if (maxBidstreamLifetimeSeconds < identityTokenExpiresAfterSeconds) { + logger.error("Max bidstream lifetime seconds ({} seconds) is less than identity token lifetime ({} seconds)", maxBidstreamLifetimeSeconds, identityTokenExpiresAfterSeconds); + return false; + } + return true; + } +} diff --git a/src/main/java/com/uid2/operator/service/DelegatingConfigService.java b/src/main/java/com/uid2/operator/service/DelegatingConfigService.java new file mode 100644 index 000000000..a34d8b1c7 --- /dev/null +++ b/src/main/java/com/uid2/operator/service/DelegatingConfigService.java @@ -0,0 +1,22 @@ +package com.uid2.operator.service; + +import io.vertx.core.json.JsonObject; + +import java.util.concurrent.atomic.AtomicReference; + +public class DelegatingConfigService implements IConfigService{ + private final AtomicReference activeConfigService; + + public DelegatingConfigService(IConfigService initialConfigService) { + this.activeConfigService = new AtomicReference<>(initialConfigService); + } + + public void updateConfigService(IConfigService newConfigService) { + this.activeConfigService.set(newConfigService); + } + + @Override + public JsonObject getConfig() { + return activeConfigService.get().getConfig(); + } +} diff --git a/src/main/java/com/uid2/operator/service/IConfigService.java b/src/main/java/com/uid2/operator/service/IConfigService.java new file mode 100644 index 000000000..0fb863242 --- /dev/null +++ b/src/main/java/com/uid2/operator/service/IConfigService.java @@ -0,0 +1,7 @@ +package com.uid2.operator.service; + +import io.vertx.core.json.JsonObject; + +public interface IConfigService { + JsonObject getConfig(); +} diff --git a/src/main/java/com/uid2/operator/service/IUIDOperatorService.java b/src/main/java/com/uid2/operator/service/IUIDOperatorService.java index c64e499fe..567f17eb4 100644 --- a/src/main/java/com/uid2/operator/service/IUIDOperatorService.java +++ b/src/main/java/com/uid2/operator/service/IUIDOperatorService.java @@ -11,9 +11,9 @@ public interface IUIDOperatorService { - IdentityTokens generateIdentity(IdentityRequest request); + IdentityTokens generateIdentity(IdentityRequest request, Duration refreshIdentityAfter, Duration refreshExpiresAfter, Duration identityExpiresAfter); - RefreshResponse refreshIdentity(RefreshToken refreshToken); + RefreshResponse refreshIdentity(RefreshToken token, Duration refreshIdentityAfter, Duration refreshExpiresAfter, Duration identityExpiresAfter); MappedIdentity mapIdentity(MapRequest request); @@ -27,6 +27,4 @@ public interface IUIDOperatorService { boolean advertisingTokenMatches(String advertisingToken, UserIdentity userIdentity, Instant asOf); Instant getLatestOptoutEntry(UserIdentity userIdentity, Instant asOf); - - Duration getIdentityExpiryDuration(); } diff --git a/src/main/java/com/uid2/operator/service/UIDOperatorService.java b/src/main/java/com/uid2/operator/service/UIDOperatorService.java index 5e66dd70c..c7e5b813c 100644 --- a/src/main/java/com/uid2/operator/service/UIDOperatorService.java +++ b/src/main/java/com/uid2/operator/service/UIDOperatorService.java @@ -9,7 +9,6 @@ import io.vertx.core.AsyncResult; import io.vertx.core.Future; import io.vertx.core.Handler; -import io.vertx.core.json.JsonObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,9 +40,6 @@ public class UIDOperatorService implements IUIDOperatorService { private final UserIdentity testValidateIdentityForPhone; private final UserIdentity testRefreshOptOutIdentityForEmail; private final UserIdentity testRefreshOptOutIdentityForPhone; - private final Duration identityExpiresAfter; - private final Duration refreshExpiresAfter; - private final Duration refreshIdentityAfter; private final OperatorIdentity operatorIdentity; private final TokenVersion refreshTokenVersion; @@ -52,8 +48,8 @@ public class UIDOperatorService implements IUIDOperatorService { private final Handler saltRetrievalResponseHandler; - public UIDOperatorService(JsonObject config, IOptOutStore optOutStore, ISaltProvider saltProvider, ITokenEncoder encoder, Clock clock, - IdentityScope identityScope, Handler saltRetrievalResponseHandler) { + public UIDOperatorService(IOptOutStore optOutStore, ISaltProvider saltProvider, ITokenEncoder encoder, Clock clock, + IdentityScope identityScope, Handler saltRetrievalResponseHandler, Boolean identityV3Enabled) { this.saltProvider = saltProvider; this.encoder = encoder; this.optOutStore = optOutStore; @@ -76,26 +72,12 @@ public UIDOperatorService(JsonObject config, IOptOutStore optOutStore, ISaltProv this.operatorIdentity = new OperatorIdentity(0, OperatorType.Service, 0, 0); - this.identityExpiresAfter = Duration.ofSeconds(config.getInteger(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS)); - this.refreshExpiresAfter = Duration.ofSeconds(config.getInteger(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS)); - this.refreshIdentityAfter = Duration.ofSeconds(config.getInteger(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS)); - - if (this.identityExpiresAfter.compareTo(this.refreshExpiresAfter) > 0) { - throw new IllegalStateException(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS + " must be >= " + IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS); - } - if (this.refreshIdentityAfter.compareTo(this.identityExpiresAfter) > 0) { - throw new IllegalStateException(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS + " must be >= " + REFRESH_IDENTITY_TOKEN_AFTER_SECONDS); - } - if (this.refreshIdentityAfter.compareTo(this.refreshExpiresAfter) > 0) { - throw new IllegalStateException(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS + " must be >= " + REFRESH_IDENTITY_TOKEN_AFTER_SECONDS); - } - this.refreshTokenVersion = TokenVersion.V3; - this.rawUidV3Enabled = config.getBoolean("identity_v3", false); + this.rawUidV3Enabled = identityV3Enabled; } @Override - public IdentityTokens generateIdentity(IdentityRequest request) { + public IdentityTokens generateIdentity(IdentityRequest request, Duration refreshIdentityAfter, Duration refreshExpiresAfter, Duration identityExpiresAfter) { final Instant now = EncodingUtils.NowUTCMillis(this.clock); final byte[] firstLevelHash = getFirstLevelHash(request.userIdentity.id, now); final UserIdentity firstLevelHashIdentity = new UserIdentity( @@ -105,12 +87,12 @@ public IdentityTokens generateIdentity(IdentityRequest request) { if (request.shouldCheckOptOut() && getGlobalOptOutResult(firstLevelHashIdentity, false).isOptedOut()) { return IdentityTokens.LogoutToken; } else { - return generateIdentity(request.publisherIdentity, firstLevelHashIdentity); + return this.generateIdentity(request.publisherIdentity, firstLevelHashIdentity, refreshIdentityAfter, refreshExpiresAfter, identityExpiresAfter); } } @Override - public RefreshResponse refreshIdentity(RefreshToken token) { + public RefreshResponse refreshIdentity(RefreshToken token, Duration refreshIdentityAfter, Duration refreshExpiresAfter, Duration identityExpiresAfter) { // should not be possible as different scopes should be using different keys, but just in case if (token.userIdentity.identityScope != this.identityScope) { return RefreshResponse.Invalid; @@ -136,7 +118,7 @@ public RefreshResponse refreshIdentity(RefreshToken token) { final Duration durationSinceLastRefresh = Duration.between(token.createdAt, now); if (!optedOut) { - IdentityTokens identityTokens = this.generateIdentity(token.publisherIdentity, token.userIdentity); + IdentityTokens identityTokens = this.generateIdentity(token.publisherIdentity, token.userIdentity, refreshIdentityAfter, refreshExpiresAfter, identityExpiresAfter); return RefreshResponse.createRefreshedResponse(identityTokens, durationSinceLastRefresh, isCstg); } else { @@ -209,11 +191,6 @@ public Instant getLatestOptoutEntry(UserIdentity userIdentity, Instant asOf) { return this.optOutStore.getLatestEntry(firstLevelHashIdentity); } - @Override - public Duration getIdentityExpiryDuration() { - return this.identityExpiresAfter; - } - private UserIdentity getFirstLevelHashIdentity(UserIdentity userIdentity, Instant asOf) { return getFirstLevelHashIdentity(userIdentity.identityScope, userIdentity.identityType, userIdentity.id, asOf); } @@ -237,7 +214,7 @@ private MappedIdentity getAdvertisingId(UserIdentity firstLevelHashIdentity, Ins rotatingSalt.getHashedId()); } - private IdentityTokens generateIdentity(PublisherIdentity publisherIdentity, UserIdentity firstLevelHashIdentity) { + private IdentityTokens generateIdentity(PublisherIdentity publisherIdentity, UserIdentity firstLevelHashIdentity, Duration refreshIdentityAfter, Duration refreshExpiresAfter, Duration identityExpiresAfter) { final Instant nowUtc = EncodingUtils.NowUTCMillis(this.clock); final MappedIdentity mappedIdentity = getAdvertisingId(firstLevelHashIdentity, nowUtc); @@ -245,14 +222,14 @@ private IdentityTokens generateIdentity(PublisherIdentity publisherIdentity, Use mappedIdentity.advertisingId, firstLevelHashIdentity.privacyBits, firstLevelHashIdentity.establishedAt, nowUtc); return this.encoder.encode( - this.createAdvertisingToken(publisherIdentity, advertisingIdentity, nowUtc), - this.createRefreshToken(publisherIdentity, firstLevelHashIdentity, nowUtc), + this.createAdvertisingToken(publisherIdentity, advertisingIdentity, nowUtc, identityExpiresAfter), + this.createRefreshToken(publisherIdentity, firstLevelHashIdentity, nowUtc, refreshExpiresAfter), nowUtc.plusMillis(refreshIdentityAfter.toMillis()), nowUtc ); } - private RefreshToken createRefreshToken(PublisherIdentity publisherIdentity, UserIdentity userIdentity, Instant now) { + private RefreshToken createRefreshToken(PublisherIdentity publisherIdentity, UserIdentity userIdentity, Instant now, Duration refreshExpiresAfter) { return new RefreshToken( this.refreshTokenVersion, now, @@ -262,7 +239,7 @@ private RefreshToken createRefreshToken(PublisherIdentity publisherIdentity, Use userIdentity); } - private AdvertisingToken createAdvertisingToken(PublisherIdentity publisherIdentity, UserIdentity userIdentity, Instant now) { + private AdvertisingToken createAdvertisingToken(PublisherIdentity publisherIdentity, UserIdentity userIdentity, Instant now, Duration identityExpiresAfter) { return new AdvertisingToken(TokenVersion.V4, now, now.plusMillis(identityExpiresAfter.toMillis()), this.operatorIdentity, publisherIdentity, userIdentity); } diff --git a/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java b/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java index 617074b9b..c0e6af231 100644 --- a/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java +++ b/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java @@ -68,6 +68,7 @@ import java.util.function.Supplier; import java.util.stream.Collectors; +import static com.uid2.operator.Const.Config.*; import static com.uid2.operator.IdentityConst.*; import static com.uid2.operator.service.ResponseUtil.*; import static com.uid2.operator.vertx.Endpoints.*; @@ -85,7 +86,7 @@ public class UIDOperatorVerticle extends AbstractVerticle { private static final String REQUEST = "request"; private final HealthComponent healthComponent = HealthManager.instance.registerComponent("http-server"); private final Cipher aesGcm; - private final JsonObject config; + private final IConfigService configService; private final boolean clientSideTokenGenerate; private final AuthMiddleware auth; private final ISiteStore siteProvider; @@ -117,9 +118,7 @@ public class UIDOperatorVerticle extends AbstractVerticle { public final static int MASTER_KEYSET_ID_FOR_SDKS = 9999999; //this is because SDKs have an issue where they assume keyset ids are always positive; that will be fixed. public final static long OPT_OUT_CHECK_CUTOFF_DATE = Instant.parse("2023-09-01T00:00:00.00Z").getEpochSecond(); private final Handler saltRetrievalResponseHandler; - private final int maxBidstreamLifetimeSeconds; private final int allowClockSkewSeconds; - protected int maxSharingLifetimeSeconds; protected Map> siteIdToInvalidOriginsAndAppNames = new HashMap<>(); protected boolean keySharingEndpointProvideAppNames; protected Instant lastInvalidOriginProcessTime = Instant.now(); @@ -136,7 +135,8 @@ public class UIDOperatorVerticle extends AbstractVerticle { private static final String ERROR_INVALID_INPUT_EMAIL_TWICE = "Only one of email or email_hash can be specified"; public final static String ORIGIN_HEADER = "Origin"; - public UIDOperatorVerticle(JsonObject config, + public UIDOperatorVerticle(IConfigService configService, + JsonObject config, boolean clientSideTokenGenerate, ISiteStore siteProvider, IClientKeyProvider clientKeyProvider, @@ -155,7 +155,7 @@ public UIDOperatorVerticle(JsonObject config, } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { throw new RuntimeException(e); } - this.config = config; + this.configService = configService; this.clientSideTokenGenerate = clientSideTokenGenerate; this.healthComponent.setHealthStatus(false, "not started"); this.auth = new AuthMiddleware(clientKeyProvider); @@ -174,14 +174,7 @@ public UIDOperatorVerticle(JsonObject config, this._statsCollectorQueue = statsCollectorQueue; this.clientKeyProvider = clientKeyProvider; this.clientSideTokenGenerateLogInvalidHttpOrigin = config.getBoolean("client_side_token_generate_log_invalid_http_origins", false); - final Integer identityTokenExpiresAfterSeconds = config.getInteger(UIDOperatorService.IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS); - this.maxBidstreamLifetimeSeconds = config.getInteger(Const.Config.MaxBidstreamLifetimeSecondsProp, identityTokenExpiresAfterSeconds); - if (this.maxBidstreamLifetimeSeconds < identityTokenExpiresAfterSeconds) { - LOGGER.error("Max bidstream lifetime seconds ({} seconds) is less than identity token lifetime ({} seconds)", maxBidstreamLifetimeSeconds, identityTokenExpiresAfterSeconds); - throw new RuntimeException("Max bidstream lifetime seconds is less than identity token lifetime seconds"); - } this.allowClockSkewSeconds = config.getInteger(Const.Config.AllowClockSkewSecondsProp, 1800); - this.maxSharingLifetimeSeconds = config.getInteger(Const.Config.MaxSharingLifetimeProp, config.getInteger(Const.Config.SharingTokenExpiryProp)); this.saltRetrievalResponseHandler = saltRetrievalResponseHandler; this.optOutStatusApiEnabled = config.getBoolean(Const.Config.OptOutStatusApiEnabled, true); this.optOutStatusMaxRequestSize = config.getInteger(Const.Config.OptOutStatusMaxRequestSize, 5000); @@ -189,15 +182,16 @@ public UIDOperatorVerticle(JsonObject config, @Override public void start(Promise startPromise) throws Exception { + Boolean identityV3Enabled = this.configService.getConfig().getBoolean(identityV3, false); this.healthComponent.setHealthStatus(false, "still starting"); this.idService = new UIDOperatorService( - this.config, this.optOutStore, this.saltProvider, this.encoder, this.clock, this.identityScope, - this.saltRetrievalResponseHandler + this.saltRetrievalResponseHandler, + identityV3Enabled ); final Router router = createRoutesSetup(); @@ -237,6 +231,11 @@ private Router createRoutesSetup() throws IOException { .allowedHeader("Content-Type")); router.route().handler(new StatsCollectorHandler(_statsCollectorQueue, vertx)); router.route("/static/*").handler(StaticHandler.create("static")); + router.route().handler(ctx -> { + JsonObject curConfig = configService.getConfig(); + ctx.put(Config, curConfig); + ctx.next(); + }); router.route().failureHandler(new GenericFailureHandler()); final BodyHandler bodyHandler = BodyHandler.create().setHandleFileUploads(false).setBodyLimit(MAX_REQUEST_BODY_SIZE); @@ -245,7 +244,7 @@ private Router createRoutesSetup() throws IOException { // Static and health check router.get(OPS_HEALTHCHECK.toString()).handler(this::handleHealthCheck); - if (this.config.getBoolean(Const.Config.AllowLegacyAPIProp, true)) { + if (this.configService.getConfig().getBoolean(Const.Config.AllowLegacyAPIProp, true)) { // V1 APIs router.get(V1_TOKEN_GENERATE.toString()).handler(auth.handleV1(this::handleTokenGenerateV1, Role.GENERATOR)); router.get(V1_TOKEN_VALIDATE.toString()).handler(this::handleTokenValidateV1); @@ -314,6 +313,9 @@ private void handleClientSideTokenGenerate(RoutingContext rc) { } } + private JsonObject getConfigFromRc(RoutingContext rc) { + return rc.get(Config); + } private Set getDomainNameListForClientSideTokenGenerate(ClientSideKeypair keypair) { Site s = siteProvider.getSite(keypair.getSiteId()); @@ -334,6 +336,13 @@ private Set getAppNames(ClientSideKeypair keypair) { private void handleClientSideTokenGenerateImpl(RoutingContext rc) throws NoSuchAlgorithmException, InvalidKeyException { final JsonObject body; + + JsonObject config = this.getConfigFromRc(rc); + + Duration refreshIdentityAfter = Duration.ofSeconds(config.getInteger(UIDOperatorService.REFRESH_IDENTITY_TOKEN_AFTER_SECONDS)); + Duration refreshExpiresAfter = Duration.ofSeconds(config.getInteger(UIDOperatorService.REFRESH_TOKEN_EXPIRES_AFTER_SECONDS)); + Duration identityExpiresAfter = Duration.ofSeconds(config.getInteger(UIDOperatorService.IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS)); + TokenResponseStatsCollector.PlatformType platformType = TokenResponseStatsCollector.PlatformType.Other; try { body = rc.body().asJsonObject(); @@ -471,7 +480,10 @@ else if(emailHash != null) { new IdentityRequest( new PublisherIdentity(clientSideKeypair.getSiteId(), 0, 0), input.toUserIdentity(this.identityScope, privacyBits.getAsInt(), Instant.now()), - OptoutCheckPolicy.RespectOptOut)); + OptoutCheckPolicy.RespectOptOut), + refreshIdentityAfter, + refreshExpiresAfter, + identityExpiresAfter); } catch (KeyManager.NoActiveKeyException e){ SendServerErrorResponseAndRecordStats(rc, "No active encryption key available", clientSideKeypair.getSiteId(), TokenResponseStatsCollector.Endpoint.ClientSideTokenGenerateV2, TokenResponseStatsCollector.ResponseStatus.NoActiveKey, siteProvider, e, platformType); return; @@ -605,11 +617,14 @@ public void handleKeysRequest(RoutingContext rc) { } } - private String getSharingTokenExpirySeconds() { - return config.getString(Const.Config.SharingTokenExpiryProp); - } +// private String getSharingTokenExpirySeconds() { +// return config.getString(Const.Config.SharingTokenExpiryProp); +// } public void handleKeysSharing(RoutingContext rc) { + JsonObject config = this.getConfigFromRc(rc); + Integer maxSharingLifetimeSeconds = config.getInteger(Const.Config.MaxSharingLifetimeProp, config.getInteger(Const.Config.SharingTokenExpiryProp)); + String sharingTokenExpirySeconds = config.getString(Const.Config.SharingTokenExpiryProp); try { final ClientKey clientKey = AuthMiddleware.getAuthClient(ClientKey.class, rc); @@ -618,7 +633,7 @@ public void handleKeysSharing(RoutingContext rc) { Map keysetMap = keyManagerSnapshot.getAllKeysets(); final JsonObject resp = new JsonObject(); - addSharingHeaderFields(resp, keyManagerSnapshot, clientKey); + addSharingHeaderFields(resp, keyManagerSnapshot, clientKey, maxSharingLifetimeSeconds, sharingTokenExpirySeconds); final List accessibleKeys = getAccessibleKeys(keysetKeyStore, keyManagerSnapshot, clientKey); @@ -663,14 +678,20 @@ public void handleKeysBidstream(RoutingContext rc) { .collect(Collectors.toList()); final JsonObject resp = new JsonObject(); - addBidstreamHeaderFields(resp); + + JsonObject config = this.getConfigFromRc(rc); + Integer identityTokenExpiresAfterSeconds = config.getInteger(UIDOperatorService.IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS); + Integer maxBidstreamLifetimeSeconds = config.getInteger(Const.Config.MaxBidstreamLifetimeSecondsProp, identityTokenExpiresAfterSeconds); + + + addBidstreamHeaderFields(resp, maxBidstreamLifetimeSeconds); resp.put("keys", keysJson); addSites(resp, accessibleKeys, keysetMap); ResponseUtil.SuccessV2(rc, resp); } - private void addBidstreamHeaderFields(JsonObject resp) { + private void addBidstreamHeaderFields(JsonObject resp, Integer maxBidstreamLifetimeSeconds) { resp.put("max_bidstream_lifetime_seconds", maxBidstreamLifetimeSeconds + TOKEN_LIFETIME_TOLERANCE.toSeconds()); addIdentityScopeField(resp); addAllowClockSkewSecondsField(resp); @@ -708,7 +729,7 @@ private void addSites(JsonObject resp, List keys, Map(apiContact, hasOriginHeader), k -> DistributionSummary .builder("uid2.token_refresh_duration_seconds") @@ -1824,7 +1892,7 @@ private void recordRefreshDurationStats(Integer siteId, String apiContact, Durat ); ds.record(durationSinceLastRefresh.getSeconds()); - boolean isExpired = durationSinceLastRefresh.compareTo(this.idService.getIdentityExpiryDuration()) > 0; + boolean isExpired = durationSinceLastRefresh.compareTo(identityExpiresAfter) > 0; Counter c = _advertisingTokenExpiryStatus.computeIfAbsent(new Tuple.Tuple3<>(String.valueOf(siteId), hasOriginHeader, isExpired), k -> Counter .builder("uid2.advertising_token_expired_on_refresh") diff --git a/src/test/java/com/uid2/operator/ConfigServiceManagerTest.java b/src/test/java/com/uid2/operator/ConfigServiceManagerTest.java new file mode 100644 index 000000000..188949562 --- /dev/null +++ b/src/test/java/com/uid2/operator/ConfigServiceManagerTest.java @@ -0,0 +1,60 @@ +package com.uid2.operator; + +import com.uid2.operator.service.ConfigServiceManager; +import com.uid2.operator.service.IConfigService; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static com.uid2.operator.Const.Config.*; +import static org.junit.jupiter.api.Assertions.*; +import static com.uid2.operator.service.UIDOperatorService.*; +import static org.mockito.Mockito.*; + +@ExtendWith(VertxExtension.class) +public class ConfigServiceManagerTest { + private JsonObject bootstrapConfig; + private JsonObject staticConfig; + private ConfigServiceManager configServiceManager; + + @BeforeEach + void setUp(Vertx vertx) { + bootstrapConfig = new JsonObject() + .put(CoreConfigPath, "/operator/config") + .put(ConfigScanPeriodMs, 300000) + .put(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS, 3600) + .put(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS, 7200) + .put(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS, 1800) + .put(MaxBidstreamLifetimeSecondsProp, 7200); + staticConfig = new JsonObject(bootstrapConfig.toString()) + .put(MaxBidstreamLifetimeSecondsProp, 7201); + + IConfigService dynamicConfigService = mock(IConfigService.class); + when(dynamicConfigService.getConfig()).thenReturn(bootstrapConfig); + IConfigService staticConfigService = mock(IConfigService.class); + when(staticConfigService.getConfig()).thenReturn(staticConfig); + + configServiceManager = new ConfigServiceManager(vertx, dynamicConfigService, staticConfigService, true); + } + + @Test + void testRemoteFeatureFlag(VertxTestContext testContext) { + IConfigService delegatingConfigService = configServiceManager.getDelegatingConfigService(); + + configServiceManager.updateConfigService(true) + .compose(updateToDynamic -> { + testContext.verify(() -> assertEquals(bootstrapConfig, delegatingConfigService.getConfig())); + + return configServiceManager.updateConfigService(false); + }) + .onSuccess(updateToStatic -> testContext.verify(() -> { + assertEquals(staticConfig, delegatingConfigService.getConfig()); + testContext.completeNow(); + })) + .onFailure(testContext::failNow); + } +} diff --git a/src/test/java/com/uid2/operator/ConfigServiceTest.java b/src/test/java/com/uid2/operator/ConfigServiceTest.java new file mode 100644 index 000000000..d7ff8e9e7 --- /dev/null +++ b/src/test/java/com/uid2/operator/ConfigServiceTest.java @@ -0,0 +1,128 @@ +package com.uid2.operator; + +import com.uid2.operator.service.ConfigRetrieverFactory; +import com.uid2.operator.service.ConfigService; +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpServer; +import io.vertx.core.json.JsonObject; +import io.vertx.config.ConfigRetriever; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.handler.BodyHandler; +import org.junit.jupiter.api.*; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.net.URISyntaxException; + +import static com.uid2.operator.Const.Config.*; +import static com.uid2.operator.service.UIDOperatorService.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(VertxExtension.class) +class ConfigServiceTest { + private Vertx vertx; + private JsonObject bootstrapConfig; + private HttpServer server; + private ConfigRetrieverFactory configRetrieverFactory; + + @BeforeEach + void setUp() { + vertx = Vertx.vertx(); + bootstrapConfig = new JsonObject() + .put(CoreConfigPath, "http://localhost:8088/operator/config") + .put(ConfigScanPeriodMs, 300000) + .put(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS, 3600) + .put(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS, 7200) + .put(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS, 1800) + .put(MaxBidstreamLifetimeSecondsProp, 7200); + + configRetrieverFactory = new ConfigRetrieverFactory(); + + } + + @AfterEach + void tearDown() { + if (server != null) { + server.close(); + } + vertx.close(); + } + + private Future startMockServer(JsonObject config) { + if (server != null) { + server.close(); + } + + Promise promise = Promise.promise(); + + Router router = Router.router(vertx); + router.route().handler(BodyHandler.create()); + router.get("/operator/config").handler(ctx -> ctx.response() + .putHeader(HttpHeaders.CONTENT_TYPE, "application/json") + .end(config.encode())); + + server = vertx.createHttpServer() + .requestHandler(router) + .listen(Const.Port.ServicePortForCore,"127.0.0.1", http -> { + if (http.succeeded()) { + promise.complete(); + } else { + promise.fail(http.cause()); + } + }); + + return promise.future(); + } + + @Test + void testGetConfig(VertxTestContext testContext) throws URISyntaxException { + ConfigRetriever configRetriever = configRetrieverFactory.createRemoteConfigRetriever(vertx, bootstrapConfig, ""); + JsonObject httpStoreConfig = bootstrapConfig; + startMockServer(httpStoreConfig) + .compose(v -> ConfigService.create(configRetriever)) + .compose(configService -> { + JsonObject retrievedConfig = configService.getConfig(); + assertNotNull(retrievedConfig, "Config retriever should initialise without error"); + assertTrue(retrievedConfig.fieldNames().containsAll(httpStoreConfig.fieldNames()), "Retrieved config should contain all keys in http store config"); + return Future.succeededFuture(); + }) + .onComplete(testContext.succeedingThenComplete()); + } + + @Test + void testInvalidConfigRevertsToPrevious(VertxTestContext testContext) { + JsonObject lastConfig = new JsonObject().put("previous", "config"); + JsonObject invalidConfig = new JsonObject() + .put(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS, 1000) + .put(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS, 2000); + ConfigRetriever spyRetriever = spy(configRetrieverFactory.createJsonRetriever(vertx, invalidConfig)); + when(spyRetriever.getCachedConfig()).thenReturn(lastConfig); + ConfigService.create(spyRetriever) + .compose(configService -> { + reset(spyRetriever); + assertEquals(lastConfig, configService.getConfig(), "Invalid config not reverted to previous config"); + return Future.succeededFuture(); + }) + .onComplete(testContext.succeedingThenComplete()); + } + + @Test + void testFirstInvalidConfigThrowsRuntimeException(VertxTestContext testContext) { + JsonObject invalidConfig = new JsonObject() + .put(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS, 1000) + .put(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS, 2000); + ConfigRetriever configRetriever = configRetrieverFactory.createJsonRetriever(vertx, invalidConfig); + ConfigService.create(configRetriever) + .onComplete(testContext.failing(throwable -> { + assertThrows(RuntimeException.class, () -> { + throw throwable; + }, "Expected a RuntimeException but the creation succeeded"); + testContext.completeNow(); + })); + } +} \ No newline at end of file diff --git a/src/test/java/com/uid2/operator/ExtendedUIDOperatorVerticle.java b/src/test/java/com/uid2/operator/ExtendedUIDOperatorVerticle.java index c90259fba..24353eaf1 100644 --- a/src/test/java/com/uid2/operator/ExtendedUIDOperatorVerticle.java +++ b/src/test/java/com/uid2/operator/ExtendedUIDOperatorVerticle.java @@ -2,6 +2,7 @@ import com.uid2.operator.model.KeyManager; import com.uid2.operator.monitoring.IStatsCollectorQueue; +import com.uid2.operator.service.IConfigService; import com.uid2.operator.service.IUIDOperatorService; import com.uid2.operator.service.SecureLinkValidatorService; import com.uid2.operator.store.IOptOutStore; @@ -17,7 +18,8 @@ //An extended UIDOperatorVerticle to expose classes for testing purposes public class ExtendedUIDOperatorVerticle extends UIDOperatorVerticle { - public ExtendedUIDOperatorVerticle(JsonObject config, + public ExtendedUIDOperatorVerticle(IConfigService configService, + JsonObject config, boolean clientSideTokenGenerate, ISiteStore siteProvider, IClientKeyProvider clientKeyProvider, @@ -29,7 +31,7 @@ public ExtendedUIDOperatorVerticle(JsonObject config, IStatsCollectorQueue statsCollectorQueue, SecureLinkValidatorService secureLinkValidationService, Handler saltRetrievalResponseHandler) { - super(config, clientSideTokenGenerate, siteProvider, clientKeyProvider, clientSideKeypairProvider, keyManager, saltProvider, optOutStore, clock, statsCollectorQueue, secureLinkValidationService, saltRetrievalResponseHandler); + super(configService, config, clientSideTokenGenerate, siteProvider, clientKeyProvider, clientSideKeypairProvider, keyManager, saltProvider, optOutStore, clock, statsCollectorQueue, secureLinkValidationService, saltRetrievalResponseHandler); } public IUIDOperatorService getIdService() { @@ -40,10 +42,6 @@ public void setKeySharingEndpointProvideAppNames(boolean enable) { this.keySharingEndpointProvideAppNames = enable; } - public void setMaxSharingLifetimeSeconds(int maxSharingLifetimeSeconds) { - this.maxSharingLifetimeSeconds = maxSharingLifetimeSeconds; - } - public void setLastInvalidOriginProcessTime(Instant lastInvalidOriginProcessTime) { this.lastInvalidOriginProcessTime = lastInvalidOriginProcessTime; } diff --git a/src/test/java/com/uid2/operator/UIDOperatorServiceTest.java b/src/test/java/com/uid2/operator/UIDOperatorServiceTest.java index 37eeef36f..a818d29df 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorServiceTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorServiceTest.java @@ -21,6 +21,8 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; + +import static com.uid2.operator.Const.Config.identityV3; import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.params.ParameterizedTest; @@ -31,6 +33,7 @@ import java.nio.charset.StandardCharsets; import java.security.Security; import java.time.Clock; +import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -54,8 +57,8 @@ public class UIDOperatorServiceTest { final int REFRESH_IDENTITY_TOKEN_AFTER_SECONDS = 300; class ExtendedUIDOperatorService extends UIDOperatorService { - public ExtendedUIDOperatorService(JsonObject config, IOptOutStore optOutStore, ISaltProvider saltProvider, ITokenEncoder encoder, Clock clock, IdentityScope identityScope, Handler saltRetrievalResponseHandler) { - super(config, optOutStore, saltProvider, encoder, clock, identityScope, saltRetrievalResponseHandler); + public ExtendedUIDOperatorService(IOptOutStore optOutStore, ISaltProvider saltProvider, ITokenEncoder encoder, Clock clock, IdentityScope identityScope, Handler saltRetrievalResponseHandler, Boolean identityV3Enabled) { + super(optOutStore, saltProvider, encoder, clock, identityScope, saltRetrievalResponseHandler, identityV3Enabled); } } @@ -88,32 +91,32 @@ void setup() throws Exception { uid2Config.put(UIDOperatorService.IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS, IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS); uid2Config.put(UIDOperatorService.REFRESH_TOKEN_EXPIRES_AFTER_SECONDS, REFRESH_TOKEN_EXPIRES_AFTER_SECONDS); uid2Config.put(UIDOperatorService.REFRESH_IDENTITY_TOKEN_AFTER_SECONDS, REFRESH_IDENTITY_TOKEN_AFTER_SECONDS); - uid2Config.put("identity_v3", false); + uid2Config.put(identityV3, false); uid2Service = new ExtendedUIDOperatorService( - uid2Config, optOutStore, saltProvider, tokenEncoder, this.clock, IdentityScope.UID2, - this.shutdownHandler::handleSaltRetrievalResponse + this.shutdownHandler::handleSaltRetrievalResponse, + uid2Config.getBoolean(identityV3) ); euidConfig = new JsonObject(); euidConfig.put(UIDOperatorService.IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS, IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS); euidConfig.put(UIDOperatorService.REFRESH_TOKEN_EXPIRES_AFTER_SECONDS, REFRESH_TOKEN_EXPIRES_AFTER_SECONDS); euidConfig.put(UIDOperatorService.REFRESH_IDENTITY_TOKEN_AFTER_SECONDS, REFRESH_IDENTITY_TOKEN_AFTER_SECONDS); - euidConfig.put("identity_v3", true); + euidConfig.put(identityV3, true); euidService = new ExtendedUIDOperatorService( - euidConfig, optOutStore, saltProvider, tokenEncoder, this.clock, IdentityScope.EUID, - this.shutdownHandler::handleSaltRetrievalResponse + this.shutdownHandler::handleSaltRetrievalResponse, + euidConfig.getBoolean(identityV3) ); } @@ -157,7 +160,11 @@ public void testGenerateAndRefresh(int siteId, TokenVersion tokenVersion) { createUserIdentity("test-email-hash", IdentityScope.UID2, IdentityType.Email), OptoutCheckPolicy.DoNotRespect ); - final IdentityTokens tokens = uid2Service.generateIdentity(identityRequest); + final IdentityTokens tokens = uid2Service.generateIdentity( + identityRequest, + Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), + Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), + Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS)); verify(shutdownHandler, atLeastOnce()).handleSaltRetrievalResponse(false); verify(shutdownHandler, never()).handleSaltRetrievalResponse(true); assertNotNull(tokens); @@ -176,7 +183,11 @@ public void testGenerateAndRefresh(int siteId, TokenVersion tokenVersion) { setNow(Instant.now().plusSeconds(200)); reset(shutdownHandler); - final RefreshResponse refreshResponse = uid2Service.refreshIdentity(refreshToken); + final RefreshResponse refreshResponse = uid2Service.refreshIdentity( + refreshToken, + Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), + Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), + Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS)); verify(shutdownHandler, atLeastOnce()).handleSaltRetrievalResponse(false); verify(shutdownHandler, never()).handleSaltRetrievalResponse(true); assertNotNull(refreshResponse); @@ -207,14 +218,22 @@ public void testTestOptOutKey_DoNotRespectOptout() { inputVal.toUserIdentity(IdentityScope.UID2, 0, this.now), OptoutCheckPolicy.DoNotRespect ); - final IdentityTokens tokens = uid2Service.generateIdentity(identityRequest); + final IdentityTokens tokens = uid2Service.generateIdentity( + identityRequest, + Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), + Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), + Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS)); verify(shutdownHandler, atLeastOnce()).handleSaltRetrievalResponse(false); verify(shutdownHandler, never()).handleSaltRetrievalResponse(true); assertNotNull(tokens); assertFalse(tokens.isEmptyToken()); final RefreshToken refreshToken = this.tokenEncoder.decodeRefreshToken(tokens.getRefreshToken()); - assertEquals(RefreshResponse.Optout, uid2Service.refreshIdentity(refreshToken)); + assertEquals(RefreshResponse.Optout, uid2Service.refreshIdentity( + refreshToken, + Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), + Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), + Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS))); } @Test @@ -226,7 +245,11 @@ public void testTestOptOutKey_RespectOptout() { inputVal.toUserIdentity(IdentityScope.UID2, 0, this.now), OptoutCheckPolicy.RespectOptOut ); - final IdentityTokens tokens = uid2Service.generateIdentity(identityRequest); + final IdentityTokens tokens = uid2Service.generateIdentity( + identityRequest, + Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), + Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), + Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS)); assertTrue(tokens.isEmptyToken()); verify(shutdownHandler, atLeastOnce()).handleSaltRetrievalResponse(false); verify(shutdownHandler, never()).handleSaltRetrievalResponse(true); @@ -242,14 +265,22 @@ public void testTestOptOutKeyIdentityScopeMismatch() { inputVal.toUserIdentity(IdentityScope.EUID, 0, this.now), OptoutCheckPolicy.DoNotRespect ); - final IdentityTokens tokens = euidService.generateIdentity(identityRequest); + final IdentityTokens tokens = euidService.generateIdentity( + identityRequest, + Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), + Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), + Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS)); verify(shutdownHandler, atLeastOnce()).handleSaltRetrievalResponse(false); verify(shutdownHandler, never()).handleSaltRetrievalResponse(true); assertNotNull(tokens); final RefreshToken refreshToken = this.tokenEncoder.decodeRefreshToken(tokens.getRefreshToken()); reset(shutdownHandler); - assertEquals(RefreshResponse.Invalid, uid2Service.refreshIdentity(refreshToken)); + assertEquals(RefreshResponse.Invalid, uid2Service.refreshIdentity( + refreshToken, + Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), + Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), + Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS))); verify(shutdownHandler, never()).handleSaltRetrievalResponse(anyBoolean()); } @@ -279,20 +310,36 @@ public void testGenerateTokenForOptOutUser(IdentityType type, String identity, I final AdvertisingToken advertisingToken; final IdentityTokens tokensAfterOptOut; if (scope == IdentityScope.UID2) { - tokens = uid2Service.generateIdentity(identityRequestForceGenerate); + tokens = uid2Service.generateIdentity( + identityRequestForceGenerate, + Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), + Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), + Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS)); verify(shutdownHandler, atLeastOnce()).handleSaltRetrievalResponse(false); verify(shutdownHandler, never()).handleSaltRetrievalResponse(true); advertisingToken = validateAndGetToken(tokenEncoder, tokens.getAdvertisingToken(), IdentityScope.UID2, userIdentity.identityType, identityRequestRespectOptOut.publisherIdentity.siteId); reset(shutdownHandler); - tokensAfterOptOut = uid2Service.generateIdentity(identityRequestRespectOptOut); + tokensAfterOptOut = uid2Service.generateIdentity( + identityRequestRespectOptOut, + Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), + Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), + Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS)); } else { - tokens = euidService.generateIdentity(identityRequestForceGenerate); + tokens = euidService.generateIdentity( + identityRequestForceGenerate, + Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), + Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), + Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS)); verify(shutdownHandler, atLeastOnce()).handleSaltRetrievalResponse(false); verify(shutdownHandler, never()).handleSaltRetrievalResponse(true); advertisingToken = validateAndGetToken(tokenEncoder, tokens.getAdvertisingToken(), IdentityScope.EUID, userIdentity.identityType, identityRequestRespectOptOut.publisherIdentity.siteId); reset(shutdownHandler); - tokensAfterOptOut = euidService.generateIdentity(identityRequestRespectOptOut); + tokensAfterOptOut = euidService.generateIdentity( + identityRequestRespectOptOut, + Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), + Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), + Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS)); } verify(shutdownHandler, atLeastOnce()).handleSaltRetrievalResponse(false); verify(shutdownHandler, never()).handleSaltRetrievalResponse(true); @@ -403,10 +450,18 @@ void testSpecialIdentityOptOutTokenGenerate(TestIdentityInputType type, String i IdentityTokens tokens; if(scope == IdentityScope.EUID) { - tokens = euidService.generateIdentity(identityRequest); + tokens = euidService.generateIdentity( + identityRequest, + Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), + Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), + Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS)); } else { - tokens = uid2Service.generateIdentity(identityRequest); + tokens = uid2Service.generateIdentity( + identityRequest, + Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), + Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), + Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS)); } verify(shutdownHandler, atLeastOnce()).handleSaltRetrievalResponse(false); verify(shutdownHandler, never()).handleSaltRetrievalResponse(true); @@ -466,10 +521,18 @@ void testSpecialIdentityOptOutTokenRefresh(TestIdentityInputType type, String id IdentityTokens tokens; if(scope == IdentityScope.EUID) { - tokens = euidService.generateIdentity(identityRequest); + tokens = euidService.generateIdentity( + identityRequest, + Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), + Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), + Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS)); } else { - tokens = uid2Service.generateIdentity(identityRequest); + tokens = uid2Service.generateIdentity( + identityRequest, + Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), + Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), + Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS)); } verify(shutdownHandler, atLeastOnce()).handleSaltRetrievalResponse(false); verify(shutdownHandler, never()).handleSaltRetrievalResponse(true); @@ -481,7 +544,11 @@ void testSpecialIdentityOptOutTokenRefresh(TestIdentityInputType type, String id final RefreshToken refreshToken = this.tokenEncoder.decodeRefreshToken(tokens.getRefreshToken()); reset(shutdownHandler); - assertEquals(RefreshResponse.Optout, (scope == IdentityScope.EUID? euidService: uid2Service).refreshIdentity(refreshToken)); + assertEquals(RefreshResponse.Optout, (scope == IdentityScope.EUID? euidService: uid2Service).refreshIdentity( + refreshToken, + Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), + Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), + Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS))); verify(shutdownHandler, never()).handleSaltRetrievalResponse(anyBoolean()); } @@ -508,10 +575,18 @@ void testSpecialIdentityRefreshOptOutGenerate(TestIdentityInputType type, String IdentityTokens tokens; if(scope == IdentityScope.EUID) { - tokens = euidService.generateIdentity(identityRequest); + tokens = euidService.generateIdentity( + identityRequest, + Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), + Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), + Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS)); } else { - tokens = uid2Service.generateIdentity(identityRequest); + tokens = uid2Service.generateIdentity( + identityRequest, + Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), + Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), + Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS)); } verify(shutdownHandler, atLeastOnce()).handleSaltRetrievalResponse(false); verify(shutdownHandler, never()).handleSaltRetrievalResponse(true); @@ -523,7 +598,11 @@ void testSpecialIdentityRefreshOptOutGenerate(TestIdentityInputType type, String final RefreshToken refreshToken = this.tokenEncoder.decodeRefreshToken(tokens.getRefreshToken()); reset(shutdownHandler); - assertEquals(RefreshResponse.Optout, (scope == IdentityScope.EUID? euidService: uid2Service).refreshIdentity(refreshToken)); + assertEquals(RefreshResponse.Optout, (scope == IdentityScope.EUID? euidService: uid2Service).refreshIdentity( + refreshToken, + Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), + Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), + Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS))); verify(shutdownHandler, never()).handleSaltRetrievalResponse(anyBoolean()); } @@ -584,10 +663,18 @@ void testSpecialIdentityValidateGenerate(TestIdentityInputType type, String id, IdentityTokens tokens; AdvertisingToken advertisingToken; if (scope == IdentityScope.EUID) { - tokens = euidService.generateIdentity(identityRequest); + tokens = euidService.generateIdentity( + identityRequest, + Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), + Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), + Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS)); } else { - tokens = uid2Service.generateIdentity(identityRequest); + tokens = uid2Service.generateIdentity( + identityRequest, + Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), + Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), + Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS)); } advertisingToken = validateAndGetToken(tokenEncoder, tokens.getAdvertisingToken(), scope, identityRequest.userIdentity.identityType, identityRequest.publisherIdentity.siteId); verify(shutdownHandler, atLeastOnce()).handleSaltRetrievalResponse(false); @@ -647,10 +734,18 @@ void testNormalIdentityOptIn(TestIdentityInputType type, String id, IdentityScop ); IdentityTokens tokens; if(scope == IdentityScope.EUID) { - tokens = euidService.generateIdentity(identityRequest); + tokens = euidService.generateIdentity( + identityRequest, + Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), + Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), + Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS)); } else { - tokens = uid2Service.generateIdentity(identityRequest); + tokens = uid2Service.generateIdentity( + identityRequest, + Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), + Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), + Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS)); } verify(shutdownHandler, atLeastOnce()).handleSaltRetrievalResponse(false); verify(shutdownHandler, never()).handleSaltRetrievalResponse(true); @@ -658,7 +753,11 @@ void testNormalIdentityOptIn(TestIdentityInputType type, String id, IdentityScop assertNotNull(tokens); final RefreshToken refreshToken = this.tokenEncoder.decodeRefreshToken(tokens.getRefreshToken()); - RefreshResponse refreshResponse = (scope == IdentityScope.EUID? euidService: uid2Service).refreshIdentity(refreshToken); + RefreshResponse refreshResponse = (scope == IdentityScope.EUID? euidService: uid2Service).refreshIdentity( + refreshToken, + Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), + Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), + Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS)); assertTrue(refreshResponse.isRefreshed()); assertNotNull(refreshResponse.getTokens()); assertNotEquals(RefreshResponse.Optout, refreshResponse); @@ -678,23 +777,23 @@ void testExpiredSaltsNotifiesShutdownHandler(TestIdentityInputType type, String saltProvider.loadContent(); UIDOperatorService uid2Service = new UIDOperatorService( - uid2Config, optOutStore, saltProvider, tokenEncoder, this.clock, IdentityScope.UID2, - this.shutdownHandler::handleSaltRetrievalResponse + this.shutdownHandler::handleSaltRetrievalResponse, + uid2Config.getBoolean(identityV3) ); UIDOperatorService euidService = new UIDOperatorService( - euidConfig, optOutStore, saltProvider, tokenEncoder, this.clock, IdentityScope.EUID, - this.shutdownHandler::handleSaltRetrievalResponse + this.shutdownHandler::handleSaltRetrievalResponse, + euidConfig.getBoolean(identityV3) ); when(this.optOutStore.getLatestEntry(any())).thenReturn(null); @@ -710,11 +809,19 @@ void testExpiredSaltsNotifiesShutdownHandler(TestIdentityInputType type, String AdvertisingToken advertisingToken; reset(shutdownHandler); if(scope == IdentityScope.EUID) { - tokens = euidService.generateIdentity(identityRequest); + tokens = euidService.generateIdentity( + identityRequest, + Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), + Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), + Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS)); advertisingToken = validateAndGetToken(tokenEncoder, tokens.getAdvertisingToken(), IdentityScope.EUID, identityRequest.userIdentity.identityType, identityRequest.publisherIdentity.siteId); } else { - tokens = uid2Service.generateIdentity(identityRequest); + tokens = uid2Service.generateIdentity( + identityRequest, + Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), + Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), + Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS)); advertisingToken = validateAndGetToken(tokenEncoder, tokens.getAdvertisingToken(), IdentityScope.UID2, identityRequest.userIdentity.identityType, identityRequest.publisherIdentity.siteId); } verify(shutdownHandler, atLeastOnce()).handleSaltRetrievalResponse(true); @@ -725,7 +832,11 @@ void testExpiredSaltsNotifiesShutdownHandler(TestIdentityInputType type, String final RefreshToken refreshToken = this.tokenEncoder.decodeRefreshToken(tokens.getRefreshToken()); reset(shutdownHandler); - RefreshResponse refreshResponse = (scope == IdentityScope.EUID? euidService: uid2Service).refreshIdentity(refreshToken); + RefreshResponse refreshResponse = (scope == IdentityScope.EUID? euidService: uid2Service).refreshIdentity( + refreshToken, + Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), + Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), + Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS)); verify(shutdownHandler, atLeastOnce()).handleSaltRetrievalResponse(true); verify(shutdownHandler, never()).handleSaltRetrievalResponse(false); assertTrue(refreshResponse.isRefreshed()); diff --git a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java index 82ab057d0..2af239977 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java @@ -114,6 +114,7 @@ public class UIDOperatorVerticleTest { @Mock private Clock clock; @Mock private IStatsCollectorQueue statsCollectorQueue; @Mock private OperatorShutdownHandler shutdownHandler; + @Mock private IConfigService configService; private SimpleMeterRegistry registry; private ExtendedUIDOperatorVerticle uidOperatorVerticle; @@ -131,8 +132,9 @@ public void deployVerticle(Vertx vertx, VertxTestContext testContext, TestInfo t if(testInfo.getDisplayName().equals("cstgNoPhoneSupport(Vertx, VertxTestContext)")) { config.put("enable_phone_support", false); } + when(configService.getConfig()).thenReturn(config); - this.uidOperatorVerticle = new ExtendedUIDOperatorVerticle(config, config.getBoolean("client_side_token_generate"), siteProvider, clientKeyProvider, clientSideKeypairProvider, new KeyManager(keysetKeyStore, keysetProvider), saltProvider, optOutStore, clock, statsCollectorQueue, secureLinkValidatorService, shutdownHandler::handleSaltRetrievalResponse); + this.uidOperatorVerticle = new ExtendedUIDOperatorVerticle(configService, config, config.getBoolean("client_side_token_generate"), siteProvider, clientKeyProvider, clientSideKeypairProvider, new KeyManager(keysetKeyStore, keysetProvider), saltProvider, optOutStore, clock, statsCollectorQueue, secureLinkValidatorService, shutdownHandler::handleSaltRetrievalResponse); vertx.deployVerticle(uidOperatorVerticle, testContext.succeeding(id -> testContext.completeNow())); @@ -4743,7 +4745,7 @@ void keyDownloadEndpointKeysets_IDREADER(boolean provideAppNames, KeyDownloadEnd @Test void keySharingKeysets_SHARER_CustomMaxSharingLifetimeSeconds(Vertx vertx, VertxTestContext testContext) { - this.uidOperatorVerticle.setMaxSharingLifetimeSeconds(999999); + this.config.put(Const.Config.MaxSharingLifetimeProp, 999999); keySharingKeysets_SHARER(true, true, vertx, testContext, 999999); } @@ -5088,4 +5090,101 @@ void secureLinkValidationFailsReturnsIdentityError(Vertx vertx, VertxTestContext testContext.completeNow(); }); } + + @Test + void tokenGenerateRespectsConfigValues(Vertx vertx, VertxTestContext testContext) { + final int clientSiteId = 201; + final String emailAddress = "test@uid2.com"; + fakeAuth(clientSiteId, Role.GENERATOR); + setupSalts(); + setupKeys(); + + String v1Param = "email=" + emailAddress; + JsonObject v2Payload = new JsonObject(); + v2Payload.put("email", emailAddress); + + Duration newIdentityExpiresAfter = Duration.ofMinutes(20); + Duration newRefreshExpiresAfter = Duration.ofMinutes(30); + Duration newRefreshIdentityAfter = Duration.ofMinutes(10); + + config.put(UIDOperatorService.IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS, newIdentityExpiresAfter.toMillis() / 1000); + config.put(UIDOperatorService.REFRESH_TOKEN_EXPIRES_AFTER_SECONDS, newRefreshExpiresAfter.toMillis() / 1000); + config.put(UIDOperatorService.REFRESH_IDENTITY_TOKEN_AFTER_SECONDS, newRefreshIdentityAfter.toMillis() / 1000); + when(configService.getConfig()).thenReturn(config); + + try { + sendTokenGenerate("v2", vertx, + v1Param, v2Payload, 200, + respJson -> { + JsonObject body = respJson.getJsonObject("body"); + testContext.verify(() -> { + assertNotNull(body); + assertEquals(now.plusMillis(newIdentityExpiresAfter.toMillis()).toEpochMilli(), body.getLong("identity_expires")); + assertEquals(now.plusMillis(newRefreshExpiresAfter.toMillis()).toEpochMilli(), body.getLong("refresh_expires")); + assertEquals(now.plusMillis(newRefreshIdentityAfter.toMillis()).toEpochMilli(), body.getLong("refresh_from")); + }); + testContext.completeNow(); + }); + } catch (Exception e) { + testContext.failNow(e); + } + } + + @Test + void keySharingRespectsConfigValues(Vertx vertx, VertxTestContext testContext) { + int newSharingTokenExpiry = config.getInteger(Const.Config.SharingTokenExpiryProp) + 1; + int newMaxSharingLifetimeSeconds = config.getInteger(Const.Config.SharingTokenExpiryProp) + 1; + + config.put(Const.Config.SharingTokenExpiryProp, newSharingTokenExpiry); + config.put(Const.Config.MaxSharingLifetimeProp, newMaxSharingLifetimeSeconds); + + String apiVersion = "v2"; + int siteId = 5; + fakeAuth(siteId, Role.SHARER); + Keyset[] keysets = { + new Keyset(MasterKeysetId, MasterKeySiteId, "test", null, now.getEpochSecond(), true, true), + new Keyset(10, 5, "siteKeyset", null, now.getEpochSecond(), true, true), + }; + KeysetKey[] encryptionKeys = { + new KeysetKey(101, "master key".getBytes(), now, now, now.plusSeconds(10), MasterKeysetId), + new KeysetKey(102, "site key".getBytes(), now, now, now.plusSeconds(10), 10), + }; + MultipleKeysetsTests test = new MultipleKeysetsTests(Arrays.asList(keysets), Arrays.asList(encryptionKeys)); + setupSiteDomainAndAppNameMock(true, false, 101, 102, 103, 104, 105); + send(apiVersion, vertx, apiVersion + "/key/sharing", true, null, null, 200, respJson -> { + + JsonObject body = respJson.getJsonObject("body"); + testContext.verify(() -> { + assertNotNull(body); + assertEquals(newSharingTokenExpiry, Integer.parseInt(body.getString("token_expiry_seconds"))); + assertEquals(newMaxSharingLifetimeSeconds + TOKEN_LIFETIME_TOLERANCE.toSeconds(), body.getLong(Const.Config.MaxSharingLifetimeProp)); + }); + testContext.completeNow(); + }); + } + + @Test + void keyBidstreamRespectsConfigValues(Vertx vertx, VertxTestContext testContext) { + int newMaxBidstreamLifetimeSeconds = 999999; + config.put(Const.Config.MaxBidstreamLifetimeSecondsProp, newMaxBidstreamLifetimeSeconds); + + final String apiVersion = "v2"; + final KeyDownloadEndpoint endpoint = KeyDownloadEndpoint.BIDSTREAM; + + final int clientSiteId = 101; + fakeAuth(clientSiteId, Role.ID_READER); + + // Required, sets up mock keys. + new MultipleKeysetsTests(); + + send(apiVersion, vertx, apiVersion + endpoint.getPath(), true, null, null, 200, respJson -> { + JsonObject body = respJson.getJsonObject("body"); + testContext.verify(() -> { + assertNotNull(body); + assertEquals(newMaxBidstreamLifetimeSeconds + TOKEN_LIFETIME_TOLERANCE.toSeconds(), body.getLong(Const.Config.MaxBidstreamLifetimeSecondsProp)); + }); + testContext.completeNow(); + }); + } + } diff --git a/src/test/java/com/uid2/operator/benchmark/BenchmarkCommon.java b/src/test/java/com/uid2/operator/benchmark/BenchmarkCommon.java index 4cc327e9f..11b0f4a3b 100644 --- a/src/test/java/com/uid2/operator/benchmark/BenchmarkCommon.java +++ b/src/test/java/com/uid2/operator/benchmark/BenchmarkCommon.java @@ -41,6 +41,8 @@ import java.util.List; import java.util.Random; +import static com.uid2.operator.Const.Config.identityV3; + public class BenchmarkCommon { static IUIDOperatorService createUidOperatorService() throws Exception { @@ -59,16 +61,9 @@ static IUIDOperatorService createUidOperatorService() throws Exception { "/com.uid2.core/test/salts/metadata.json"); saltProvider.loadContent(); - final int IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS = 600; - final int REFRESH_TOKEN_EXPIRES_AFTER_SECONDS = 900; - final int REFRESH_IDENTITY_TOKEN_AFTER_SECONDS = 300; - - final JsonObject config = new JsonObject(); - config.put(UIDOperatorService.IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS, IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS); - config.put(UIDOperatorService.REFRESH_TOKEN_EXPIRES_AFTER_SECONDS, REFRESH_TOKEN_EXPIRES_AFTER_SECONDS); - config.put(UIDOperatorService.REFRESH_IDENTITY_TOKEN_AFTER_SECONDS, REFRESH_IDENTITY_TOKEN_AFTER_SECONDS); + final JsonObject config = getConfig(); - final EncryptedTokenEncoder tokenEncoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + final EncryptedTokenEncoder tokenEncoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); final List optOutPartitionFiles = new ArrayList<>(); final ICloudStorage optOutLocalStorage = make1mOptOutEntryStorage( saltProvider.getSnapshot(Instant.now()).getFirstLevelSalt(), @@ -76,16 +71,28 @@ static IUIDOperatorService createUidOperatorService() throws Exception { final IOptOutStore optOutStore = new StaticOptOutStore(optOutLocalStorage, make1mOptOutEntryConfig(), optOutPartitionFiles); return new UIDOperatorService( - config, optOutStore, saltProvider, tokenEncoder, Clock.systemUTC(), IdentityScope.UID2, - null + null, + config.getBoolean(identityV3) ); } + public static JsonObject getConfig() { + final int IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS = 600; + final int REFRESH_TOKEN_EXPIRES_AFTER_SECONDS = 900; + final int REFRESH_IDENTITY_TOKEN_AFTER_SECONDS = 300; + + final JsonObject config = new JsonObject(); + config.put(UIDOperatorService.IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS, IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS); + config.put(UIDOperatorService.REFRESH_TOKEN_EXPIRES_AFTER_SECONDS, REFRESH_TOKEN_EXPIRES_AFTER_SECONDS); + config.put(UIDOperatorService.REFRESH_IDENTITY_TOKEN_AFTER_SECONDS, REFRESH_IDENTITY_TOKEN_AFTER_SECONDS); + return config; + } + static EncryptedTokenEncoder createTokenEncoder() throws Exception { RotatingKeysetKeyStore keysetKeyStore = new RotatingKeysetKeyStore( new EmbeddedResourceStorage(Main.class), diff --git a/src/test/java/com/uid2/operator/benchmark/TokenEndecBenchmark.java b/src/test/java/com/uid2/operator/benchmark/TokenEndecBenchmark.java index aaa821db9..d1fba74be 100644 --- a/src/test/java/com/uid2/operator/benchmark/TokenEndecBenchmark.java +++ b/src/test/java/com/uid2/operator/benchmark/TokenEndecBenchmark.java @@ -3,13 +3,17 @@ import com.uid2.operator.model.*; import com.uid2.operator.service.EncryptedTokenEncoder; import com.uid2.operator.service.IUIDOperatorService; +import io.vertx.core.json.JsonObject; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Mode; +import java.time.Duration; import java.util.ArrayList; import java.util.List; +import static com.uid2.operator.service.UIDOperatorService.*; + public class TokenEndecBenchmark { private static final IUIDOperatorService uidService; @@ -18,6 +22,7 @@ public class TokenEndecBenchmark { private static final EncryptedTokenEncoder encoder; private static final IdentityTokens[] generatedTokens; private static int idx = 0; + private static final JsonObject config; static { try { @@ -29,6 +34,7 @@ public class TokenEndecBenchmark { if (generatedTokens.length < 65536 || userIdentities.length < 65536) { throw new IllegalStateException("must create more than 65535 test candidates."); } + config = BenchmarkCommon.getConfig(); } catch (Exception e) { throw new RuntimeException(e); } @@ -38,10 +44,14 @@ static IdentityTokens[] createAdvertisingTokens() { List tokens = new ArrayList<>(); for (int i = 0; i < userIdentities.length; i++) { tokens.add( - uidService.generateIdentity(new IdentityRequest( - publisher, - userIdentities[i], - OptoutCheckPolicy.DoNotRespect))); + uidService.generateIdentity( + new IdentityRequest( + publisher, + userIdentities[i], + OptoutCheckPolicy.DoNotRespect), + Duration.ofSeconds(config.getInteger(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS)), + Duration.ofSeconds(config.getInteger(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS)), + Duration.ofSeconds(config.getInteger(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS)))); } return tokens.toArray(new IdentityTokens[tokens.size()]); } @@ -52,7 +62,10 @@ public IdentityTokens TokenGenerationBenchmark() { return uidService.generateIdentity(new IdentityRequest( publisher, userIdentities[(idx++) & 65535], - OptoutCheckPolicy.DoNotRespect)); + OptoutCheckPolicy.DoNotRespect), + Duration.ofSeconds(config.getInteger(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS)), + Duration.ofSeconds(config.getInteger(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS)), + Duration.ofSeconds(config.getInteger(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS))); } @Benchmark @@ -60,6 +73,9 @@ public IdentityTokens TokenGenerationBenchmark() { public RefreshResponse TokenRefreshBenchmark() { return uidService.refreshIdentity( encoder.decodeRefreshToken( - generatedTokens[(idx++) & 65535].getRefreshToken())); + generatedTokens[(idx++) & 65535].getRefreshToken()), + Duration.ofSeconds(config.getInteger(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS)), + Duration.ofSeconds(config.getInteger(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS)), + Duration.ofSeconds(config.getInteger(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS))); } } diff --git a/src/test/java/com/uid2/operator/service/ConfigValidatorUtilTest.java b/src/test/java/com/uid2/operator/service/ConfigValidatorUtilTest.java new file mode 100644 index 000000000..dde9b6080 --- /dev/null +++ b/src/test/java/com/uid2/operator/service/ConfigValidatorUtilTest.java @@ -0,0 +1,59 @@ +package com.uid2.operator.service; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class ConfigValidatorUtilTest { + @Test + void testValidateIdentityRefreshTokens() { + // identityExpiresAfter is greater than refreshExpiresAfter + assertFalse(ConfigValidatorUtil.validateIdentityRefreshTokens(10, 5, 3)); + + // refreshIdentityAfter is greater than identityExpiresAfter + assertFalse(ConfigValidatorUtil.validateIdentityRefreshTokens(5, 10, 6)); + + // refreshIdentityAfter is greater than refreshExpiresAfter + assertFalse(ConfigValidatorUtil.validateIdentityRefreshTokens(5, 10, 11)); + + // all conditions are valid + assertTrue(ConfigValidatorUtil.validateIdentityRefreshTokens(5, 10, 3)); + } + + @Test + void testValidateBidstreamLifetime() { + // maxBidstreamLifetimeSeconds is less than identityTokenExpiresAfterSeconds + assertFalse(ConfigValidatorUtil.validateBidstreamLifetime(5, 10)); + + // maxBidstreamLifetimeSeconds is greater than or equal to identityTokenExpiresAfterSeconds + assertTrue(ConfigValidatorUtil.validateBidstreamLifetime(10, 5)); + assertTrue(ConfigValidatorUtil.validateBidstreamLifetime(10, 10)); + } + + @Test + void testValidateIdentityRefreshTokensWithNullValues() { + // identityExpiresAfter is null + assertFalse(ConfigValidatorUtil.validateIdentityRefreshTokens(null, 10, 5)); + + // refreshExpiresAfter is null + assertFalse(ConfigValidatorUtil.validateIdentityRefreshTokens(10, null, 5)); + + // refreshIdentityAfter is null + assertFalse(ConfigValidatorUtil.validateIdentityRefreshTokens(10, 5, null)); + + // all values are null + assertFalse(ConfigValidatorUtil.validateIdentityRefreshTokens(null, null, null)); + } + + @Test + void testValidateBidstreamLifetimeWithNullValues() { + // maxBidstreamLifetimeSeconds is null + assertFalse(ConfigValidatorUtil.validateBidstreamLifetime(null, 10)); + + // identityTokenExpiresAfterSeconds is null + assertFalse(ConfigValidatorUtil.validateBidstreamLifetime(10, null)); + + // both values are null + assertFalse(ConfigValidatorUtil.validateBidstreamLifetime(null, null)); + } +}