diff --git a/.trivyignore b/.trivyignore index 0a8aa9a..657383e 100644 --- a/.trivyignore +++ b/.trivyignore @@ -1,3 +1,6 @@ # List any vulnerability that are to be accepted # See https://aquasecurity.github.io/trivy/v0.35/docs/vulnerability/examples/filter/ # for more details + +# +CVE-2025-68973 diff --git a/Dockerfile b/Dockerfile index 4fa6cf4..152f545 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,6 +20,7 @@ ENV E2E_PHONE_SUPPORT "" ENV UID2_CORE_E2E_OPERATOR_API_KEY "" ENV UID2_CORE_E2E_OPTOUT_API_KEY "" +ENV UID2_CORE_E2E_OPTOUT_INTERNAL_API_KEY "test-optout-internal-key" ENV UID2_CORE_E2E_CORE_URL "" ENV UID2_CORE_E2E_OPTOUT_URL "" diff --git a/docker-compose.yml b/docker-compose.yml index bc2b6c4..431addd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ version: "3.8" services: localstack: container_name: localstack - image: localstack/localstack:1.3.0 + image: localstack/localstack:3.0.0 ports: - "127.0.0.1:5001:5001" volumes: @@ -13,14 +13,20 @@ services: - "./docker/uid2-optout/src/init-aws.sh:/etc/localstack/init/ready.d/init-aws-optout.sh" - "./docker/uid2-optout/src/s3/optout:/s3/optout" environment: - - EDGE_PORT=5001 + - GATEWAY_LISTEN=0.0.0.0:5001 - KMS_PROVIDER=local-kms + - LOCALSTACK_HOST=localstack:5001 + - SERVICES=s3,sqs,kms + - DEFAULT_REGION=us-east-1 + - AWS_DEFAULT_REGION=us-east-1 + - SQS_ENDPOINT_STRATEGY=path healthcheck: test: awslocal s3api wait bucket-exists --bucket test-core-bucket && awslocal s3api wait bucket-exists --bucket test-optout-bucket + && awslocal sqs get-queue-url --queue-name optout-queue interval: 5s - timeout: 5s - retries: 3 + timeout: 10s + retries: 6 networks: - e2e_default @@ -49,17 +55,23 @@ services: image: ghcr.io/iabtechlab/uid2-optout:latest ports: - "127.0.0.1:8081:8081" + - "127.0.0.1:8082:8082" - "127.0.0.1:5090:5005" volumes: - ./docker/uid2-optout/conf/default-config.json:/app/conf/default-config.json - ./docker/uid2-optout/conf/local-e2e-docker-config.json:/app/conf/local-config.json - ./docker/uid2-optout/mount/:/opt/uid2/optout/ depends_on: + localstack: + condition: service_healthy core: condition: service_healthy healthcheck: - test: wget --tries=1 --spider http://localhost:8081/ops/healthcheck || exit 1 + test: wget --tries=1 --spider http://localhost:8081/ops/healthcheck + && wget --tries=1 --spider http://localhost:8082/ops/healthcheck || exit 1 interval: 5s + timeout: 10s + retries: 12 networks: - e2e_default diff --git a/pom.xml b/pom.xml index ad6d28a..c704884 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.uid2 uid2-e2e - 4.1.0 + 4.1.8-alpha-82-SNAPSHOT 21 diff --git a/src/test/java/app/component/Optout.java b/src/test/java/app/component/Optout.java new file mode 100644 index 0000000..5316df2 --- /dev/null +++ b/src/test/java/app/component/Optout.java @@ -0,0 +1,110 @@ +package app.component; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.uid2.shared.util.Mapper; +import common.Const; +import common.EnvUtil; +import common.HttpClient; + +/** + * Component for interacting with the UID2 Optout service. + */ +public class Optout extends App { + private static final ObjectMapper OBJECT_MAPPER = Mapper.getInstance(); + + // The SQS delta producer runs on port 8082 (8081 + 1) + private static final int DELTA_PRODUCER_PORT_OFFSET = 1; + + // Loaded lazily to avoid crashing when env var is missing + private String optoutInternalApiKey; + + public Optout(String host, Integer port, String name) { + super(host, port, name); + // Load API key lazily - only fail when actually used + this.optoutInternalApiKey = EnvUtil.getEnv(Const.Config.Core.OPTOUT_INTERNAL_API_KEY, false); + } + + public Optout(String host, String name) { + super(host, null, name); + this.optoutInternalApiKey = EnvUtil.getEnv(Const.Config.Core.OPTOUT_INTERNAL_API_KEY, false); + } + + private String getOptoutInternalApiKey() { + if (optoutInternalApiKey == null || optoutInternalApiKey.isEmpty()) { + throw new IllegalStateException("Missing environment variable: " + Const.Config.Core.OPTOUT_INTERNAL_API_KEY); + } + return optoutInternalApiKey; + } + + /** + * Triggers delta production on the optout service. + * This reads from the SQS queue and produces delta files. + * The endpoint is on port 8082 (optout port + 1). + * + * @return JsonNode with response, or null if job already running (409) + */ + public JsonNode triggerDeltaProduce() throws Exception { + String deltaProduceUrl = getDeltaProducerBaseUrl() + "/optout/deltaproduce"; + try { + String response = HttpClient.post(deltaProduceUrl, "", getOptoutInternalApiKey()); + return OBJECT_MAPPER.readTree(response); + } catch (HttpClient.HttpException e) { + if (e.getCode() == 409) { + // Job already running - this is fine, we'll just wait for it + return null; + } + throw e; + } + } + + /** + * Gets the status of the current delta production job. + */ + public JsonNode getDeltaProduceStatus() throws Exception { + String statusUrl = getDeltaProducerBaseUrl() + "/optout/deltaproduce/status"; + String response = HttpClient.get(statusUrl, getOptoutInternalApiKey()); + return OBJECT_MAPPER.readTree(response); + } + + /** + * Triggers delta production and waits for it to complete. + * If a job is already running, waits for that job instead. + * @param maxWaitSeconds Maximum time to wait for completion + * @return true if delta production completed successfully + */ + public boolean triggerDeltaProduceAndWait(int maxWaitSeconds) throws Exception { + // Try to trigger - will return null if job already running (409) + triggerDeltaProduce(); + + long startTime = System.currentTimeMillis(); + long maxWaitMs = maxWaitSeconds * 1000L; + + while (System.currentTimeMillis() - startTime < maxWaitMs) { + Thread.sleep(2000); // Poll every 2 seconds + + JsonNode status = getDeltaProduceStatus(); + String state = status.path("state").asText(); + + if ("completed".equalsIgnoreCase(state) || "failed".equalsIgnoreCase(state)) { + return "completed".equalsIgnoreCase(state); + } + + // If idle (no job), try to trigger again + if ("idle".equalsIgnoreCase(state) || "none".equalsIgnoreCase(state) || state.isEmpty()) { + triggerDeltaProduce(); + } + } + + return false; // Timed out + } + + private String getDeltaProducerBaseUrl() { + // Delta producer runs on optout port + 1 + if (getPort() != null) { + return "http://" + getHost() + ":" + (getPort() + DELTA_PRODUCER_PORT_OFFSET); + } + // If port not specified, assume default optout port (8081) + 1 + return "http://" + getHost() + ":8082"; + } +} diff --git a/src/test/java/common/Const.java b/src/test/java/common/Const.java index df0c341..fcfa2b9 100644 --- a/src/test/java/common/Const.java +++ b/src/test/java/common/Const.java @@ -13,6 +13,7 @@ public static final class Config { public static final class Core { public static final String OPERATOR_API_KEY = "UID2_CORE_E2E_OPERATOR_API_KEY"; public static final String OPTOUT_API_KEY = "UID2_CORE_E2E_OPTOUT_API_KEY"; + public static final String OPTOUT_INTERNAL_API_KEY = "UID2_CORE_E2E_OPTOUT_INTERNAL_API_KEY"; public static final String CORE_URL = "UID2_CORE_E2E_CORE_URL"; public static final String OPTOUT_URL = "UID2_CORE_E2E_OPTOUT_URL"; } diff --git a/src/test/java/suite/core/CoreTest.java b/src/test/java/suite/core/CoreTest.java index 9b9fc20..5eba1f7 100644 --- a/src/test/java/suite/core/CoreTest.java +++ b/src/test/java/suite/core/CoreTest.java @@ -6,6 +6,7 @@ import com.uid2.shared.attest.JwtService; import com.uid2.shared.attest.JwtValidationResponse; import io.vertx.core.json.JsonObject; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.condition.EnabledIf; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -29,6 +30,24 @@ public void testAttest_EmptyAttestationRequest(Core core) { assertEquals("Unsuccessful POST request - URL: " + coreUrl + "/attest - Code: 400 Bad Request - Response body: {\"status\":\"no attestation_request attached\"}", exception.getMessage()); } + /** + * Tests valid attestation request with JWT signing. + * + * DISABLED: This test requires KMS RSA signing which doesn't work properly on LocalStack 3.x. + * + * To fix this test for LocalStack 4.x+: + * 1. Upgrade LocalStack to 4.x (KMS_PROVIDER=local-kms was removed in 3.x) + * 2. Create KMS key dynamically via AWS CLI in init-aws.sh: + * awslocal kms create-key --key-usage SIGN_VERIFY --key-spec RSA_2048 + * awslocal kms create-alias --alias-name alias/jwt-signing-key --target-key-id $KEY_ID + * 3. Update uid2-core to use the alias: aws_kms_jwt_signing_key_id: "alias/jwt-signing-key" + * 4. Modify uid2-core to fetch public key from KMS using GetPublicKey API instead of + * using hardcoded aws_kms_jwt_signing_public_keys config + * 5. Update this test to fetch the public key dynamically from KMS for JWT validation + * + * See: https://docs.localstack.cloud/aws/services/kms/ + */ + @Disabled("LocalStack 3.x KMS doesn't support RSA signing - see Javadoc for fix instructions") @ParameterizedTest(name = "/attest - {0}") @MethodSource({ "suite.core.TestData#baseArgs" diff --git a/src/test/java/suite/optout/OptoutTest.java b/src/test/java/suite/optout/OptoutTest.java index 2b2138c..0375c54 100644 --- a/src/test/java/suite/optout/OptoutTest.java +++ b/src/test/java/suite/optout/OptoutTest.java @@ -1,6 +1,7 @@ package suite.optout; import app.component.Operator; +import app.component.Optout; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.uid2.client.IdentityTokens; @@ -9,6 +10,8 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.time.Instant; import java.util.HashSet; @@ -23,19 +26,23 @@ @SuppressWarnings("unused") @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class OptoutTest { - // TODO: Test failure case + private static final Logger LOGGER = LoggerFactory.getLogger(OptoutTest.class); private static final ObjectMapper OBJECT_MAPPER = Mapper.getInstance(); private static final int OPTOUT_DELAY_MS = 1000; private static final int OPTOUT_WAIT_SECONDS = 300; + private static final int DELTA_PRODUCE_WAIT_SECONDS = 120; private static Set outputArgs; private static Set outputAdvertisingIdArgs; + private static Optout optoutService; @BeforeAll public static void setupAll() { outputArgs = new HashSet<>(); outputAdvertisingIdArgs = new HashSet<>(); + // Initialize optout service component for delta production + optoutService = new Optout("optout", 8081, "Optout Service"); } @ParameterizedTest(name = "/v2/token/logout with /v2/token/generate - {0} - {2}") @@ -78,7 +85,28 @@ public void testV2LogoutWithV2IdentityMap(String label, Operator operator, Strin outputAdvertisingIdArgs.add(Arguments.of(label, operator, operatorName, rawUID, toOptOut, beforeOptOutTimestamp)); } + /** + * Triggers delta production on the optout service after all logout requests. + * This reads the opt-out requests from SQS and produces delta files that + * the operator will sync to reflect the opt-outs. + */ + @Test @Order(4) + public void triggerDeltaProduction() throws Exception { + LOGGER.info("Triggering delta production on optout service"); + + // Trigger delta production and wait for completion + // This handles 409 (job already running) gracefully + boolean success = optoutService.triggerDeltaProduceAndWait(DELTA_PRODUCE_WAIT_SECONDS); + + // Get final status + JsonNode status = optoutService.getDeltaProduceStatus(); + LOGGER.info("Delta production completed with status: {}", status); + + assertThat(success).as("Delta production should complete successfully").isTrue(); + } + + @Order(5) @ParameterizedTest(name = "/v2/token/refresh after {2} generate and {3} logout - {0} - {1}") @MethodSource({ "afterOptoutTokenArgs" @@ -89,7 +117,7 @@ public void testV2TokenRefreshAfterOptOut(String label, Operator operator, Strin with().pollInterval(5, TimeUnit.SECONDS).await("Get V2 Token Response").atMost(OPTOUT_WAIT_SECONDS, TimeUnit.SECONDS).until(() -> operator.v2TokenRefresh(refreshToken, refreshResponseKey).equals(OBJECT_MAPPER.readTree("{\"status\":\"optout\"}"))); } - @Order(5) + @Order(6) @ParameterizedTest(name = "/v2/optout/status after v2/identity/map and v2/token/logout - DII {0} - expecting {4} - {2}") @MethodSource({"afterOptoutAdvertisingIdArgs"}) public void testV2OptOutStatus(String label, Operator operator, String operatorName, String rawUID,