Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.iterable.iterableapi;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

class ApiEndpointClassification {

private static final Set<String> DEFAULT_UNAUTHENTICATED = new HashSet<>(Arrays.asList(
IterableConstants.ENDPOINT_DISABLE_DEVICE,
IterableConstants.ENDPOINT_GET_REMOTE_CONFIGURATION,
IterableConstants.ENDPOINT_MERGE_USER,
IterableConstants.ENDPOINT_CRITERIA_LIST,
IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION,
IterableConstants.ENDPOINT_TRACK_CONSENT
));

private volatile Set<String> unauthenticatedPaths = new HashSet<>(DEFAULT_UNAUTHENTICATED);

boolean requiresJwt(String path) {
return !unauthenticatedPaths.contains(path);
}

void updateFromRemoteConfig(Set<String> paths) {
this.unauthenticatedPaths = new HashSet<>(paths);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public class IterableApi {
private IterableHelper.FailureHandler _setUserFailureCallbackHandler;

IterableApiClient apiClient = new IterableApiClient(new IterableApiAuthProvider());
final ApiEndpointClassification apiEndpointClassification = new ApiEndpointClassification();
private static final UnknownUserMerge unknownUserMerge = new UnknownUserMerge();
private @Nullable UnknownUserManager unknownUserManager;
private @Nullable IterableInAppManager inAppManager;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ class IterableTask {
this.taskType = taskType;
}

boolean requiresJwt(ApiEndpointClassification classification) {
return classification.requiresJwt(this.name);
}

}

enum IterableTaskType {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class IterableTaskRunner implements IterableTaskStorage.TaskCreatedListener, Han
private IterableActivityMonitor activityMonitor;
private IterableNetworkConnectivityManager networkConnectivityManager;
private HealthMonitor healthMonitor;
private ApiEndpointClassification classification;

private static final int RETRY_INTERVAL_SECONDS = 60;

Expand All @@ -45,18 +46,28 @@ interface TaskCompletedListener {
IterableTaskRunner(IterableTaskStorage taskStorage,
IterableActivityMonitor activityMonitor,
IterableNetworkConnectivityManager networkConnectivityManager,
HealthMonitor healthMonitor) {
HealthMonitor healthMonitor,
ApiEndpointClassification classification) {
this.taskStorage = taskStorage;
this.activityMonitor = activityMonitor;
this.networkConnectivityManager = networkConnectivityManager;
this.healthMonitor = healthMonitor;
this.classification = classification;
networkThread.start();
handler = new Handler(networkThread.getLooper(), this);
taskStorage.addTaskCreatedListener(this);
networkConnectivityManager.addNetworkListener(this);
activityMonitor.addCallback(this);
}

// Preserved for backward compatibility with existing tests
IterableTaskRunner(IterableTaskStorage taskStorage,
IterableActivityMonitor activityMonitor,
IterableNetworkConnectivityManager networkConnectivityManager,
HealthMonitor healthMonitor) {
this(taskStorage, activityMonitor, networkConnectivityManager, healthMonitor, new ApiEndpointClassification());
}

void addTaskCompletedListener(TaskCompletedListener listener) {
taskCompletedListeners.add(listener);
}
Expand Down Expand Up @@ -130,13 +141,7 @@ private void processTasks() {
boolean autoRetry = IterableApi.getInstance().isAutoRetryOnJwtFailure();

while (networkConnectivityManager.isConnected()) {
// [F] When autoRetry is enabled, also check that auth token is ready before processing
if (autoRetry && !IterableApi.getInstance().getAuthManager().isAuthTokenReady()) {
IterableLogger.d(TAG, "Auth token not ready, pausing task processing");
return;
}

IterableTask task = taskStorage.getNextScheduledTask();
IterableTask task = getNextActionableTask(autoRetry);

if (task == null) {
return;
Expand All @@ -154,10 +159,21 @@ private void processTasks() {
}
}

private IterableTask getNextActionableTask(boolean autoRetry) {
boolean authBlocked = isPausedForAuth ||
(autoRetry && !IterableApi.getInstance().getAuthManager().isAuthTokenReady());
if (!authBlocked) {
return taskStorage.getNextScheduledTask();
}
return taskStorage.getNextScheduledTaskNotRequiringJwt(classification);
}

void setIsPausedForAuth(boolean paused) {
this.isPausedForAuth = paused;
}

@WorkerThread
private boolean processTask(@NonNull IterableTask task, boolean autoRetry) {
isPausedForAuth = false;

if (task.taskType == IterableTaskType.API) {
IterableApiResponse response = null;
TaskResult result = TaskResult.FAILURE;
Expand All @@ -177,7 +193,7 @@ private boolean processTask(@NonNull IterableTask task, boolean autoRetry) {
if (response.success) {
result = TaskResult.SUCCESS;
} else {
// [F] If autoRetry is enabled and response is a 401 JWT error,
// If autoRetry is enabled and response is a 401 JWT error,
// retain the task and pause processing until a valid JWT is obtained.
if (autoRetry && isJwtFailure(response)) {
IterableLogger.d(TAG, "JWT auth failure on task " + task.id + ". Retaining task and pausing processing.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,34 @@ IterableTask getNextScheduledTask() {
return task;
}

/**
* Returns the next scheduled task that does not require JWT authentication.
* Iterates tasks ordered by scheduledAt and returns the first one classified
* as unauthenticated by the given classification.
*
* @param classification the endpoint classification to check against
* @return next unauthenticated {@link IterableTask}, or null if none found
*/
@Nullable
IterableTask getNextScheduledTaskNotRequiringJwt(ApiEndpointClassification classification) {
if (!isDatabaseReady()) {
return null;
}
Cursor cursor = database.rawQuery("select * from OfflineTask order by scheduled", null);
IterableTask task = null;
if (cursor.moveToFirst()) {
do {
IterableTask candidate = createTaskFromCursor(cursor);
if (!candidate.requiresJwt(classification)) {
task = candidate;
break;
}
} while (cursor.moveToNext());
}
cursor.close();
return task;
}

/**
* Deletes all the entries from the OfflineTask table.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,12 @@ class OfflineRequestProcessor implements RequestProcessor {
IterableNetworkConnectivityManager networkConnectivityManager = IterableNetworkConnectivityManager.sharedInstance(context);
taskStorage = IterableTaskStorage.sharedInstance(context);
healthMonitor = new HealthMonitor(taskStorage);
ApiEndpointClassification classification = IterableApi.getInstance().apiEndpointClassification;
taskRunner = new IterableTaskRunner(taskStorage,
IterableActivityMonitor.getInstance(),
networkConnectivityManager,
healthMonitor);
healthMonitor,
classification);
taskScheduler = new TaskScheduler(taskStorage, taskRunner);

// Register task runner as auth token ready listener for JWT auto-retry support
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.iterable.iterableapi;

import org.junit.Before;
import org.junit.Test;

import java.util.Arrays;
import java.util.HashSet;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public class ApiEndpointClassificationTest {

private ApiEndpointClassification classification;

@Before
public void setUp() {
classification = new ApiEndpointClassification();
}

@Test
public void testDefaultUnauthenticatedEndpoints() {
// THIS IS IMPORTANT SO IF WE CHANGE IT FOR TESTING WE WILL HAVE THIS FAILING
assertFalse(classification.requiresJwt(IterableConstants.ENDPOINT_DISABLE_DEVICE));
assertFalse(classification.requiresJwt(IterableConstants.ENDPOINT_GET_REMOTE_CONFIGURATION));
assertFalse(classification.requiresJwt(IterableConstants.ENDPOINT_MERGE_USER));
assertFalse(classification.requiresJwt(IterableConstants.ENDPOINT_CRITERIA_LIST));
assertFalse(classification.requiresJwt(IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION));
assertFalse(classification.requiresJwt(IterableConstants.ENDPOINT_TRACK_CONSENT));
}

@Test
public void testUnknownEndpointRequiresJwt() {
assertTrue(classification.requiresJwt("unknown/endpoint"));
}

@Test
public void testUpdateFromRemoteConfigOverridesDefaults() {
// Override: now only "events/track" is unauthenticated
classification.updateFromRemoteConfig(
new HashSet<>(Arrays.asList(IterableConstants.ENDPOINT_TRACK))
);

assertFalse(classification.requiresJwt(IterableConstants.ENDPOINT_TRACK));
// Previously unauthenticated endpoints now require JWT
assertTrue(classification.requiresJwt(IterableConstants.ENDPOINT_DISABLE_DEVICE));
assertTrue(classification.requiresJwt(IterableConstants.ENDPOINT_MERGE_USER));
}

@Test
public void testIterableTaskRequiresJwtDelegation() {
IterableTask authTask = new IterableTask(IterableConstants.ENDPOINT_TRACK, IterableTaskType.API, "{}");
IterableTask unauthTask = new IterableTask(IterableConstants.ENDPOINT_DISABLE_DEVICE, IterableTaskType.API, "{}");

assertTrue(authTask.requiresJwt(classification));
assertFalse(unauthTask.requiresJwt(classification));
}
}
Loading
Loading