Addons may register additional rules after Identica is initialized.
+ *
+ * resolve();
+}
diff --git a/identica-api/src/main/java/me/whereareiam/identica/identity/session/recognition/eligibility/RecognitionEligibilityRule.java b/identica-api/src/main/java/me/whereareiam/identica/identity/session/recognition/eligibility/RecognitionEligibilityRule.java
new file mode 100644
index 000000000..b1dd7adda
--- /dev/null
+++ b/identica-api/src/main/java/me/whereareiam/identica/identity/session/recognition/eligibility/RecognitionEligibilityRule.java
@@ -0,0 +1,63 @@
+package me.whereareiam.identica.identity.session.recognition.eligibility;
+
+import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityContext;
+import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityRuleDecision;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Rule that contributes to recognition eligibility evaluation.
+ *
+ * Rules run in ascending {@link #order()} and may abstain, allow, or block
+ * recognition for the current context.
+ *
+ * {@code
+ * public final class CorporateRule implements RecognitionEligibilityRule {
+ * public @NotNull String id() {
+ * return "corporate";
+ * }
+ *
+ * public int order() {
+ * return 50;
+ * }
+ *
+ * public @NotNull RecognitionEligibilityRuleDecision evaluate(
+ * @NotNull RecognitionEligibilityContext context
+ * ) {
+ * return RecognitionEligibilityRuleDecision.abstain();
+ * }
+ * }
+ * }
+ */
+public interface RecognitionEligibilityRule {
+ /**
+ * Stable rule identifier used for registration and replacement.
+ *
+ * @return stable rule id
+ */
+ @NotNull String id();
+
+ /**
+ * Evaluation order. Lower values run first.
+ *
+ * @return rule order
+ */
+ int order();
+
+ /**
+ * Optional guard for context-specific rules.
+ *
+ * @param context recognition attempt context
+ * @return {@code true} when this rule should evaluate
+ */
+ default boolean supports(@NotNull RecognitionEligibilityContext context) {
+ return true;
+ }
+
+ /**
+ * Evaluates this rule for the given context.
+ *
+ * @param context recognition attempt context
+ * @return per-rule decision
+ */
+ @NotNull RecognitionEligibilityRuleDecision evaluate(@NotNull RecognitionEligibilityContext context);
+}
diff --git a/identica-api/src/main/java/me/whereareiam/identica/identity/session/recognition/eligibility/RecognitionEligibilityService.java b/identica-api/src/main/java/me/whereareiam/identica/identity/session/recognition/eligibility/RecognitionEligibilityService.java
new file mode 100644
index 000000000..72c4cfc47
--- /dev/null
+++ b/identica-api/src/main/java/me/whereareiam/identica/identity/session/recognition/eligibility/RecognitionEligibilityService.java
@@ -0,0 +1,35 @@
+package me.whereareiam.identica.identity.session.recognition.eligibility;
+
+import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityContext;
+import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityDecision;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Resolves whether recognition may be attempted for a connection context.
+ *
+ * This check runs before any stored recognition snapshot is matched.
+ * It allows core and addon rules to block or restore recognition eligibility
+ * for reconnect and provider-scoped handshake recognition paths.
+ *
+ * {@code
+ * RecognitionEligibilityDecision decision = service.evaluate(
+ * RecognitionEligibilityContext.builder()
+ * .providerId("premium")
+ * .attemptKind(RecognitionAttemptKind.SESSION_RECOGNITION)
+ * .trigger(RecognitionTrigger.AUTOMATIC)
+ * .build()
+ * );
+ * if (!decision.isAllowed()) {
+ * return false;
+ * }
+ * }
+ */
+public interface RecognitionEligibilityService {
+ /**
+ * Evaluates whether recognition may proceed for the given context.
+ *
+ * @param context recognition attempt context
+ * @return resolved eligibility decision
+ */
+ @NotNull RecognitionEligibilityDecision evaluate(@NotNull RecognitionEligibilityContext context);
+}
diff --git a/identica-api/src/main/java/me/whereareiam/identica/identity/session/recognition/policy/UntrustedIpRecognitionDecision.java b/identica-api/src/main/java/me/whereareiam/identica/identity/session/recognition/policy/UntrustedIpRecognitionDecision.java
deleted file mode 100644
index e44762c85..000000000
--- a/identica-api/src/main/java/me/whereareiam/identica/identity/session/recognition/policy/UntrustedIpRecognitionDecision.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package me.whereareiam.identica.identity.session.recognition.policy;
-
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-import lombok.ToString;
-import org.jetbrains.annotations.NotNull;
-
-/**
- * Result of evaluating whether automatic reconnect recognition may proceed for a connection.
- */
-@Getter
-@ToString
-@AllArgsConstructor
-public class UntrustedIpRecognitionDecision {
- private final @NotNull Outcome outcome;
-
- /**
- * Returns whether automatic reconnect recognition must be blocked.
- *
- * @return {@code true} when automatic recognition should be suppressed
- */
- public boolean isBlocked() {
- return outcome == Outcome.BLOCKED_UNTRUSTED_IP;
- }
-
- /**
- * Outcome of untrusted-IP recognition evaluation.
- */
- public enum Outcome {
- ALLOWED_DISABLED,
- ALLOWED_MISSING_IP,
- ALLOWED_IP_NOT_MATCHED,
- ALLOWED_PROVIDER_OVERRIDE,
- ALLOWED_EXPLICIT_SELECTION,
- BLOCKED_UNTRUSTED_IP
- }
-}
diff --git a/identica-api/src/main/java/me/whereareiam/identica/identity/session/recognition/policy/UntrustedIpRecognitionPolicy.java b/identica-api/src/main/java/me/whereareiam/identica/identity/session/recognition/policy/UntrustedIpRecognitionPolicy.java
deleted file mode 100644
index 579818d69..000000000
--- a/identica-api/src/main/java/me/whereareiam/identica/identity/session/recognition/policy/UntrustedIpRecognitionPolicy.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package me.whereareiam.identica.identity.session.recognition.policy;
-
-import me.whereareiam.identica.model.provider.ProviderContext;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-/**
- * Evaluates whether automatic reconnect recognition should be suppressed for a connection.
- */
-public interface UntrustedIpRecognitionPolicy {
- /**
- * Evaluates the automatic reconnect-recognition decision for the given provider and connection.
- *
- * @param providerId provider being auto-considered
- * @param clientIp current client IP
- * @param selectedProvider current scenario provider context
- * @return resolved recognition decision
- */
- @NotNull UntrustedIpRecognitionDecision evaluateAutomaticRecognition(
- @Nullable String providerId,
- @Nullable String clientIp,
- @Nullable ProviderContext selectedProvider
- );
-
- /**
- * Returns whether the given client IP matches a configured untrusted IP or CIDR entry.
- *
- * @param clientIp current client IP
- * @return {@code true} when the IP is configured as untrusted
- */
- boolean isUntrustedIp(@Nullable String clientIp);
-}
diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/config/Providers.java b/identica-api/src/main/java/me/whereareiam/identica/model/config/Providers.java
index 4a7008af1..56ed2193a 100644
--- a/identica-api/src/main/java/me/whereareiam/identica/model/config/Providers.java
+++ b/identica-api/src/main/java/me/whereareiam/identica/model/config/Providers.java
@@ -9,8 +9,8 @@
import me.whereareiam.configura.ConfigDocument;
import me.whereareiam.configura.annotation.Merge;
import me.whereareiam.configura.merge.strategy.type.StructuralObject;
-import me.whereareiam.identica.type.session.RecognitionSignal;
import me.whereareiam.identica.type.session.SessionConcurrencyPolicy;
+import me.whereareiam.identica.type.session.recognition.RecognitionSignal;
import me.whereareiam.identica.type.verification.UnavailableSelectionPolicy;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -97,10 +97,6 @@ public static class ProviderEntry {
@Setter
@ToString
public static class Overrides {
- /**
- * Restores automatic reconnect recognition for this provider even when the client IP is configured as untrusted.
- */
- private boolean allowRecognitionOnUntrustedIp;
private @Nullable SessionConcurrencyPolicy sessionConcurrencyPolicy;
private @NotNull Recognition recognition = new Recognition();
@@ -110,6 +106,14 @@ public static class Overrides {
public static class Recognition {
private @Nullable Boolean enabled;
private @NotNull List signals = new ArrayList<>();
+ private @NotNull Eligibility eligibility = new Eligibility();
+
+ @Getter
+ @Setter
+ @ToString
+ public static class Eligibility {
+ private boolean allowOnUntrustedIp;
+ }
}
}
diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/config/Settings.java b/identica-api/src/main/java/me/whereareiam/identica/model/config/Settings.java
index 03ab7fa26..ca2dfa030 100644
--- a/identica-api/src/main/java/me/whereareiam/identica/model/config/Settings.java
+++ b/identica-api/src/main/java/me/whereareiam/identica/model/config/Settings.java
@@ -14,8 +14,8 @@
import me.whereareiam.identica.type.pipeline.PipelineConcurrencyPolicy;
import me.whereareiam.identica.type.pipeline.journey.JourneyMode;
import me.whereareiam.identica.type.pipeline.journey.JourneyPolicy;
-import me.whereareiam.identica.type.session.RecognitionSignal;
import me.whereareiam.identica.type.session.SessionConcurrencyPolicy;
+import me.whereareiam.identica.type.session.recognition.RecognitionSignal;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -226,9 +226,9 @@ public static class Recognition {
*/
private @NotNull List defaultSignals = new ArrayList<>();
/**
- * Guard configuration that suppresses automatic reconnect recognition from configured client IP ranges.
+ * Eligibility rules for whether recognition may be attempted.
*/
- private @NotNull UntrustedIps untrustedIps = new UntrustedIps();
+ private @NotNull Eligibility eligibility = new Eligibility();
public long validityMillis() {
if (validity.isZero() || validity.isNegative())
@@ -238,20 +238,30 @@ public long validityMillis() {
}
/**
- * Guard configuration for automatic reconnect recognition coming from untrusted client IPs.
+ * Eligibility rules for automatic reconnect recognition.
*/
@Getter
@Setter
@ToString
- public static class UntrustedIps {
- /**
- * Enables the untrusted-IP suppression guard.
- */
- private boolean enabled;
+ public static class Eligibility {
+ private @NotNull UntrustedIps untrustedIps = new UntrustedIps();
+
/**
- * Exact IPs or CIDR ranges that should suppress automatic reconnect recognition.
+ * Untrusted-IP rules for whether automatic recognition may run.
*/
- private @NotNull List entries = new ArrayList<>();
+ @Getter
+ @Setter
+ @ToString
+ public static class UntrustedIps {
+ /**
+ * Enables the untrusted-IP eligibility rule.
+ */
+ private boolean enabled;
+ /**
+ * Exact IPs or CIDR ranges that suppress recognition eligibility.
+ */
+ private @NotNull List entries = new ArrayList<>();
+ }
}
}
}
diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/session/recognition/eligibility/RecognitionEligibilityContext.java b/identica-api/src/main/java/me/whereareiam/identica/model/session/recognition/eligibility/RecognitionEligibilityContext.java
new file mode 100644
index 000000000..9c08a03fc
--- /dev/null
+++ b/identica-api/src/main/java/me/whereareiam/identica/model/session/recognition/eligibility/RecognitionEligibilityContext.java
@@ -0,0 +1,29 @@
+package me.whereareiam.identica.model.session.recognition.eligibility;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.ToString;
+import me.whereareiam.identica.identity.actor.ConnectionIdentity;
+import me.whereareiam.identica.model.provider.ProviderContext;
+import me.whereareiam.identica.type.session.recognition.RecognitionAttemptKind;
+import me.whereareiam.identica.type.session.recognition.RecognitionTrigger;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Recognition attempt context used to decide whether recognition may run.
+ */
+@Getter
+@ToString
+@Builder(toBuilder = true)
+public class RecognitionEligibilityContext {
+ private final @Nullable String providerId;
+ private final @Nullable String providerUsername;
+ private final @Nullable String clientIp;
+
+ private final @Nullable ProviderContext selectedProvider;
+ private final @Nullable ConnectionIdentity.Origin origin;
+
+ private final @NotNull RecognitionAttemptKind attemptKind;
+ private final @NotNull RecognitionTrigger trigger;
+}
diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/session/recognition/eligibility/RecognitionEligibilityDecision.java b/identica-api/src/main/java/me/whereareiam/identica/model/session/recognition/eligibility/RecognitionEligibilityDecision.java
new file mode 100644
index 000000000..179f7e70d
--- /dev/null
+++ b/identica-api/src/main/java/me/whereareiam/identica/model/session/recognition/eligibility/RecognitionEligibilityDecision.java
@@ -0,0 +1,18 @@
+package me.whereareiam.identica.model.session.recognition.eligibility;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.ToString;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Final eligibility decision for a recognition attempt.
+ */
+@Getter
+@ToString
+@Builder(toBuilder = true)
+public class RecognitionEligibilityDecision {
+ private final boolean allowed;
+ private final @Nullable String reason;
+ private final @Nullable String ruleId;
+}
diff --git a/identica-api/src/main/java/me/whereareiam/identica/model/session/recognition/eligibility/RecognitionEligibilityRuleDecision.java b/identica-api/src/main/java/me/whereareiam/identica/model/session/recognition/eligibility/RecognitionEligibilityRuleDecision.java
new file mode 100644
index 000000000..4e2682b27
--- /dev/null
+++ b/identica-api/src/main/java/me/whereareiam/identica/model/session/recognition/eligibility/RecognitionEligibilityRuleDecision.java
@@ -0,0 +1,58 @@
+package me.whereareiam.identica.model.session.recognition.eligibility;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.ToString;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Decision produced by a single recognition eligibility rule.
+ */
+@Getter
+@ToString
+@Builder(toBuilder = true)
+@AllArgsConstructor
+public class RecognitionEligibilityRuleDecision {
+ private final @NotNull Status status;
+ private final @Nullable String reason;
+
+ /**
+ * Creates an abstain decision.
+ *
+ * @return abstain decision
+ */
+ public static @NotNull RecognitionEligibilityRuleDecision abstain() {
+ return new RecognitionEligibilityRuleDecision(Status.ABSTAIN, null);
+ }
+
+ /**
+ * Creates an allow decision.
+ *
+ * @param reason optional allow reason
+ * @return allow decision
+ */
+ public static @NotNull RecognitionEligibilityRuleDecision allow(@Nullable String reason) {
+ return new RecognitionEligibilityRuleDecision(Status.ALLOW, reason);
+ }
+
+ /**
+ * Creates a block decision.
+ *
+ * @param reason optional block reason
+ * @return block decision
+ */
+ public static @NotNull RecognitionEligibilityRuleDecision block(@Nullable String reason) {
+ return new RecognitionEligibilityRuleDecision(Status.BLOCK, reason);
+ }
+
+ /**
+ * Per-rule recognition eligibility result.
+ */
+ public enum Status {
+ ABSTAIN,
+ ALLOW,
+ BLOCK
+ }
+}
diff --git a/identica-api/src/main/java/me/whereareiam/identica/type/session/recognition/RecognitionAttemptKind.java b/identica-api/src/main/java/me/whereareiam/identica/type/session/recognition/RecognitionAttemptKind.java
new file mode 100644
index 000000000..5be227912
--- /dev/null
+++ b/identica-api/src/main/java/me/whereareiam/identica/type/session/recognition/RecognitionAttemptKind.java
@@ -0,0 +1,9 @@
+package me.whereareiam.identica.type.session.recognition;
+
+/**
+ * Recognition attempt category that is being evaluated.
+ */
+public enum RecognitionAttemptKind {
+ SESSION_RECOGNITION,
+ PROVIDER_HANDSHAKE_RECOGNITION
+}
diff --git a/identica-api/src/main/java/me/whereareiam/identica/type/session/RecognitionSignal.java b/identica-api/src/main/java/me/whereareiam/identica/type/session/recognition/RecognitionSignal.java
similarity index 71%
rename from identica-api/src/main/java/me/whereareiam/identica/type/session/RecognitionSignal.java
rename to identica-api/src/main/java/me/whereareiam/identica/type/session/recognition/RecognitionSignal.java
index a32aa0bd0..3a9385159 100644
--- a/identica-api/src/main/java/me/whereareiam/identica/type/session/RecognitionSignal.java
+++ b/identica-api/src/main/java/me/whereareiam/identica/type/session/recognition/RecognitionSignal.java
@@ -1,4 +1,4 @@
-package me.whereareiam.identica.type.session;
+package me.whereareiam.identica.type.session.recognition;
/**
* Signals that may be matched before a reconnecting player is recognized.
diff --git a/identica-api/src/main/java/me/whereareiam/identica/type/session/recognition/RecognitionTrigger.java b/identica-api/src/main/java/me/whereareiam/identica/type/session/recognition/RecognitionTrigger.java
new file mode 100644
index 000000000..971964f3f
--- /dev/null
+++ b/identica-api/src/main/java/me/whereareiam/identica/type/session/recognition/RecognitionTrigger.java
@@ -0,0 +1,9 @@
+package me.whereareiam.identica.type.session.recognition;
+
+/**
+ * Trigger that caused a recognition attempt to be evaluated.
+ */
+public enum RecognitionTrigger {
+ AUTOMATIC,
+ EXPLICIT_PROVIDER_SELECTION
+}
diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/CommonConfiguration.java b/identica-common/src/main/java/me/whereareiam/identica/common/CommonConfiguration.java
index 06694492d..75d0440ec 100644
--- a/identica-common/src/main/java/me/whereareiam/identica/common/CommonConfiguration.java
+++ b/identica-common/src/main/java/me/whereareiam/identica/common/CommonConfiguration.java
@@ -30,7 +30,11 @@
import me.whereareiam.identica.common.identity.session.DefaultSessionService;
import me.whereareiam.identica.common.identity.session.recognition.DefaultSessionRecognitionService;
import me.whereareiam.identica.common.identity.session.recognition.DefaultSessionRecognitionStore;
-import me.whereareiam.identica.common.identity.session.recognition.policy.UntrustedIpRecognitionGuard;
+import me.whereareiam.identica.common.identity.session.recognition.eligibility.DefaultRecognitionEligibilityRegistry;
+import me.whereareiam.identica.common.identity.session.recognition.eligibility.DefaultRecognitionEligibilityService;
+import me.whereareiam.identica.common.identity.session.recognition.eligibility.rule.ExplicitSelectionRecognitionEligibilityRule;
+import me.whereareiam.identica.common.identity.session.recognition.eligibility.rule.RecognitionEnabledEligibilityRule;
+import me.whereareiam.identica.common.identity.session.recognition.eligibility.rule.UntrustedIpRecognitionEligibilityRule;
import me.whereareiam.identica.common.listener.DefaultDynamicListenerRegistry;
import me.whereareiam.identica.common.listener.SessionClosedDisconnectListener;
import me.whereareiam.identica.common.listener.SessionReplacedListener;
@@ -70,7 +74,8 @@
import me.whereareiam.identica.identity.session.SessionService;
import me.whereareiam.identica.identity.session.recognition.SessionRecognitionService;
import me.whereareiam.identica.identity.session.recognition.SessionRecognitionStore;
-import me.whereareiam.identica.identity.session.recognition.policy.UntrustedIpRecognitionPolicy;
+import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityRegistry;
+import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityService;
import me.whereareiam.identica.listener.DynamicListenerRegistry;
import me.whereareiam.identica.logging.BannerContributor;
import me.whereareiam.identica.model.config.*;
@@ -176,6 +181,8 @@ protected void configure() {
bind(SessionService.class).to(DefaultSessionService.class).asEagerSingleton();
bind(SessionRecognitionStore.class).to(DefaultSessionRecognitionStore.class).asEagerSingleton();
bind(SessionRecognitionService.class).to(DefaultSessionRecognitionService.class).asEagerSingleton();
+ bind(RecognitionEligibilityRegistry.class).to(DefaultRecognitionEligibilityRegistry.class).asEagerSingleton();
+ bind(RecognitionEligibilityService.class).to(DefaultRecognitionEligibilityService.class).asEagerSingleton();
// Platform adaptation helpers
bind(ConnectionDecisionApplier.class).asEagerSingleton();
@@ -207,7 +214,6 @@ protected void configure() {
// Provider system
bind(ProviderDescriptorReader.class).to(DefaultProviderDescriptorReader.class).asEagerSingleton();
bind(ProviderManager.class).to(DefaultProviderManager.class).asEagerSingleton();
- bind(UntrustedIpRecognitionPolicy.class).to(UntrustedIpRecognitionGuard.class).asEagerSingleton();
bind(ProviderOperations.class).to(DefaultProviderOperations.class).asEagerSingleton();
// Core services
@@ -227,6 +233,18 @@ void initializeEventUtil(EventManager eventManager) {
EventUtil.initialize(eventManager);
}
+ @Inject
+ void initializeRecognitionEligibilityRules(
+ RecognitionEligibilityRegistry registry,
+ RecognitionEnabledEligibilityRule recognitionEnabledEligibilityRule,
+ ExplicitSelectionRecognitionEligibilityRule explicitSelectionRecognitionEligibilityRule,
+ UntrustedIpRecognitionEligibilityRule untrustedIpRecognitionEligibilityRule
+ ) {
+ registry.register(recognitionEnabledEligibilityRule);
+ registry.register(explicitSelectionRecognitionEligibilityRule);
+ registry.register(untrustedIpRecognitionEligibilityRule);
+ }
+
@Inject
void initializeConfigura() {
Path configuredDataPath = ensureDirectory(dataPath, "data");
diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/SettingsDefaults.java b/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/SettingsDefaults.java
index 7ddde4b1a..67fdd61e9 100644
--- a/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/SettingsDefaults.java
+++ b/identica-common/src/main/java/me/whereareiam/identica/common/config/defaults/SettingsDefaults.java
@@ -12,8 +12,8 @@
import me.whereareiam.identica.type.pipeline.PipelineConcurrencyPolicy;
import me.whereareiam.identica.type.pipeline.journey.JourneyMode;
import me.whereareiam.identica.type.pipeline.journey.JourneyPolicy;
-import me.whereareiam.identica.type.session.RecognitionSignal;
import me.whereareiam.identica.type.session.SessionConcurrencyPolicy;
+import me.whereareiam.identica.type.session.recognition.RecognitionSignal;
import java.time.Duration;
import java.util.HashMap;
@@ -46,7 +46,8 @@ public Settings supply(Settings settings) {
RecognitionSignal.IP,
RecognitionSignal.VIRTUAL_HOST
));
- Settings.Sessions.Recognition.UntrustedIps untrustedIps = new Settings.Sessions.Recognition.UntrustedIps();
+ Settings.Sessions.Recognition.Eligibility.UntrustedIps untrustedIps =
+ new Settings.Sessions.Recognition.Eligibility.UntrustedIps();
untrustedIps.setEnabled(true);
untrustedIps.setEntries(List.of(
"127.0.0.1",
@@ -55,7 +56,9 @@ public Settings supply(Settings settings) {
"172.16.0.0/12",
"192.168.0.0/16"
));
- recognition.setUntrustedIps(untrustedIps);
+ Settings.Sessions.Recognition.Eligibility eligibility = new Settings.Sessions.Recognition.Eligibility();
+ eligibility.setUntrustedIps(untrustedIps);
+ recognition.setEligibility(eligibility);
sessions.setRecognition(recognition);
Settings.Connection connection = new Settings.Connection();
diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/DefaultSessionRecognitionService.java b/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/DefaultSessionRecognitionService.java
index 0328c6d0d..5b8ccab76 100644
--- a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/DefaultSessionRecognitionService.java
+++ b/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/DefaultSessionRecognitionService.java
@@ -7,11 +7,14 @@
import me.whereareiam.identica.identity.actor.ConnectionIdentity;
import me.whereareiam.identica.identity.session.recognition.SessionRecognitionService;
import me.whereareiam.identica.identity.session.recognition.SessionRecognitionStore;
-import me.whereareiam.identica.identity.session.recognition.policy.UntrustedIpRecognitionPolicy;
+import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityService;
import me.whereareiam.identica.model.config.Providers;
import me.whereareiam.identica.model.config.Settings;
import me.whereareiam.identica.model.session.SessionRecognitionSnapshot;
-import me.whereareiam.identica.type.session.RecognitionSignal;
+import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityContext;
+import me.whereareiam.identica.type.session.recognition.RecognitionAttemptKind;
+import me.whereareiam.identica.type.session.recognition.RecognitionSignal;
+import me.whereareiam.identica.type.session.recognition.RecognitionTrigger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -26,7 +29,7 @@ public class DefaultSessionRecognitionService implements SessionRecognitionServi
private final Provider settingsProvider;
private final Provider providersProvider;
private final SessionRecognitionStore sessionRecognitionStore;
- private final UntrustedIpRecognitionPolicy untrustedIpRecognitionPolicy;
+ private final RecognitionEligibilityService recognitionEligibilityService;
@Override
public boolean matches(
@@ -38,7 +41,14 @@ public boolean matches(
) {
if (isBlank(providerId) || isBlank(providerSubject) || !isRecognitionEnabled(providerId))
return false;
- if (untrustedIpRecognitionPolicy.evaluateAutomaticRecognition(providerId, ip, null).isBlocked())
+ if (!recognitionEligibilityService.evaluate(RecognitionEligibilityContext.builder()
+ .providerId(providerId)
+ .providerUsername(providerUsername)
+ .clientIp(ip)
+ .origin(origin)
+ .attemptKind(RecognitionAttemptKind.SESSION_RECOGNITION)
+ .trigger(RecognitionTrigger.AUTOMATIC)
+ .build()).isAllowed())
return false;
Optional storedOptional = sessionRecognitionStore.find(providerId.trim(), providerSubject.trim());
diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/DefaultRecognitionEligibilityRegistry.java b/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/DefaultRecognitionEligibilityRegistry.java
new file mode 100644
index 000000000..3bf7830c5
--- /dev/null
+++ b/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/DefaultRecognitionEligibilityRegistry.java
@@ -0,0 +1,42 @@
+package me.whereareiam.identica.common.identity.session.recognition.eligibility;
+
+import com.google.inject.Singleton;
+import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityRegistry;
+import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityRule;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+@Singleton
+public class DefaultRecognitionEligibilityRegistry implements RecognitionEligibilityRegistry {
+ private final List rules = new CopyOnWriteArrayList<>();
+
+ @Override
+ public void register(@NotNull RecognitionEligibilityRule rule) {
+ String id = rule.id();
+ if (id.isBlank()) return;
+
+ String normalized = normalize(id);
+ rules.removeIf(existing -> existing != null && normalize(existing.id()).equals(normalized));
+ rules.add(rule);
+ }
+
+ @Override
+ public boolean unregister(@NotNull String ruleId) {
+ if (ruleId.isBlank()) return false;
+
+ String normalized = normalize(ruleId);
+ return rules.removeIf(existing -> existing != null && normalize(existing.id()).equals(normalized));
+ }
+
+ @Override
+ public @NotNull List resolve() {
+ return List.copyOf(rules);
+ }
+
+ private @NotNull String normalize(@NotNull String value) {
+ return value.trim().toLowerCase(Locale.ROOT);
+ }
+}
diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/DefaultRecognitionEligibilityService.java b/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/DefaultRecognitionEligibilityService.java
new file mode 100644
index 000000000..5d701286b
--- /dev/null
+++ b/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/DefaultRecognitionEligibilityService.java
@@ -0,0 +1,62 @@
+package me.whereareiam.identica.common.identity.session.recognition.eligibility;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import lombok.RequiredArgsConstructor;
+import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityRegistry;
+import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityRule;
+import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityService;
+import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityContext;
+import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityDecision;
+import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityRuleDecision;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Comparator;
+
+@Singleton
+@RequiredArgsConstructor(onConstructor_ = @Inject)
+public class DefaultRecognitionEligibilityService implements RecognitionEligibilityService {
+ private final RecognitionEligibilityRegistry registry;
+
+ @Override
+ public @NotNull RecognitionEligibilityDecision evaluate(@NotNull RecognitionEligibilityContext context) {
+ RecognitionEligibilityDecision allowed = RecognitionEligibilityDecision.builder()
+ .allowed(true)
+ .reason("no-rule-blocked")
+ .build();
+
+ return registry.resolve().stream()
+ .sorted(Comparator.comparingInt(RecognitionEligibilityRule::order)
+ .thenComparing(rule -> rule.id().trim().toLowerCase(java.util.Locale.ROOT)))
+ .filter(rule -> rule.supports(context))
+ .reduce(
+ allowed,
+ (current, rule) -> evaluateRule(current, rule, context),
+ (left, right) -> right
+ );
+ }
+
+ private @NotNull RecognitionEligibilityDecision evaluateRule(
+ @NotNull RecognitionEligibilityDecision current,
+ @NotNull RecognitionEligibilityRule rule,
+ @NotNull RecognitionEligibilityContext context
+ ) {
+ if (!current.isAllowed()) return current;
+
+ RecognitionEligibilityRuleDecision decision = rule.evaluate(context);
+ if (decision.getStatus() == RecognitionEligibilityRuleDecision.Status.ABSTAIN) return current;
+ if (decision.getStatus() == RecognitionEligibilityRuleDecision.Status.BLOCK)
+ return RecognitionEligibilityDecision.builder()
+ .allowed(false)
+ .reason(decision.getReason())
+ .ruleId(rule.id())
+ .build();
+
+ if (current.getRuleId() != null) return current;
+ return RecognitionEligibilityDecision.builder()
+ .allowed(true)
+ .reason(decision.getReason())
+ .ruleId(rule.id())
+ .build();
+ }
+}
diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/policy/matcher/UntrustedIpMatcher.java b/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/matcher/IpMatcher.java
similarity index 76%
rename from identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/policy/matcher/UntrustedIpMatcher.java
rename to identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/matcher/IpMatcher.java
index eefd132b3..867683c00 100644
--- a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/policy/matcher/UntrustedIpMatcher.java
+++ b/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/matcher/IpMatcher.java
@@ -1,9 +1,9 @@
-package me.whereareiam.identica.common.identity.session.recognition.policy.matcher;
+package me.whereareiam.identica.common.identity.session.recognition.eligibility.matcher;
import org.jetbrains.annotations.NotNull;
import java.net.InetAddress;
-public interface UntrustedIpMatcher {
+public interface IpMatcher {
boolean matches(@NotNull InetAddress address);
}
diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/matcher/type/CidrMatcher.java b/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/matcher/type/CidrMatcher.java
new file mode 100644
index 000000000..a502cbe00
--- /dev/null
+++ b/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/matcher/type/CidrMatcher.java
@@ -0,0 +1,30 @@
+package me.whereareiam.identica.common.identity.session.recognition.eligibility.matcher.type;
+
+import lombok.RequiredArgsConstructor;
+import me.whereareiam.identica.common.identity.session.recognition.eligibility.matcher.IpMatcher;
+import org.jetbrains.annotations.NotNull;
+
+import java.net.InetAddress;
+import java.util.Arrays;
+
+@RequiredArgsConstructor
+public final class CidrMatcher implements IpMatcher {
+ private final @NotNull InetAddress baseAddress;
+ private final int prefixLength;
+
+ @Override
+ public boolean matches(@NotNull InetAddress address) {
+ byte[] left = baseAddress.getAddress();
+ byte[] right = address.getAddress();
+ if (left.length != right.length) return false;
+
+ int fullBytes = prefixLength / 8;
+ int remainingBits = prefixLength % 8;
+ if (!Arrays.equals(Arrays.copyOf(left, fullBytes), Arrays.copyOf(right, fullBytes)))
+ return false;
+ if (remainingBits == 0) return true;
+
+ int mask = -(1 << (8 - remainingBits)) & 0xFF;
+ return (left[fullBytes] & mask) == (right[fullBytes] & mask);
+ }
+}
diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/matcher/type/ExactIpMatcher.java b/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/matcher/type/ExactIpMatcher.java
new file mode 100644
index 000000000..8e26c60c0
--- /dev/null
+++ b/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/matcher/type/ExactIpMatcher.java
@@ -0,0 +1,17 @@
+package me.whereareiam.identica.common.identity.session.recognition.eligibility.matcher.type;
+
+import lombok.RequiredArgsConstructor;
+import me.whereareiam.identica.common.identity.session.recognition.eligibility.matcher.IpMatcher;
+import org.jetbrains.annotations.NotNull;
+
+import java.net.InetAddress;
+
+@RequiredArgsConstructor
+public final class ExactIpMatcher implements IpMatcher {
+ private final @NotNull InetAddress address;
+
+ @Override
+ public boolean matches(@NotNull InetAddress value) {
+ return address.equals(value);
+ }
+}
diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/ExplicitSelectionRecognitionEligibilityRule.java b/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/ExplicitSelectionRecognitionEligibilityRule.java
new file mode 100644
index 000000000..5a30f1e9e
--- /dev/null
+++ b/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/ExplicitSelectionRecognitionEligibilityRule.java
@@ -0,0 +1,44 @@
+package me.whereareiam.identica.common.identity.session.recognition.eligibility.rule;
+
+import com.google.inject.Singleton;
+import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityRule;
+import me.whereareiam.identica.model.provider.ProviderContext;
+import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityContext;
+import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityRuleDecision;
+import me.whereareiam.identica.type.provider.ProviderOrigin;
+import me.whereareiam.identica.type.session.recognition.RecognitionAttemptKind;
+import org.jetbrains.annotations.NotNull;
+
+@Singleton
+public class ExplicitSelectionRecognitionEligibilityRule implements RecognitionEligibilityRule {
+ @Override
+ public @NotNull String id() {
+ return "explicit-selection";
+ }
+
+ @Override
+ public int order() {
+ return 50;
+ }
+
+ @Override
+ public boolean supports(@NotNull RecognitionEligibilityContext context) {
+ return context.getAttemptKind() == RecognitionAttemptKind.PROVIDER_HANDSHAKE_RECOGNITION;
+ }
+
+ @Override
+ public @NotNull RecognitionEligibilityRuleDecision evaluate(@NotNull RecognitionEligibilityContext context) {
+ ProviderContext provider = context.getSelectedProvider();
+ String providerId = context.getProviderId();
+ if (provider == null || providerId == null || providerId.isBlank())
+ return RecognitionEligibilityRuleDecision.abstain();
+ if (provider.getProviderId() == null || !provider.getProviderId().equalsIgnoreCase(providerId))
+ return RecognitionEligibilityRuleDecision.abstain();
+
+ ProviderOrigin source = provider.getSource();
+ if (source == ProviderOrigin.ENTRYPOINT || source == ProviderOrigin.MANUAL)
+ return RecognitionEligibilityRuleDecision.allow("explicit-provider-selection");
+
+ return RecognitionEligibilityRuleDecision.abstain();
+ }
+}
diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/RecognitionEnabledEligibilityRule.java b/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/RecognitionEnabledEligibilityRule.java
new file mode 100644
index 000000000..6e47233f6
--- /dev/null
+++ b/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/RecognitionEnabledEligibilityRule.java
@@ -0,0 +1,66 @@
+package me.whereareiam.identica.common.identity.session.recognition.eligibility.rule;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import lombok.RequiredArgsConstructor;
+import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityRule;
+import me.whereareiam.identica.model.config.Providers;
+import me.whereareiam.identica.model.config.Settings;
+import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityContext;
+import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityRuleDecision;
+import me.whereareiam.identica.type.session.recognition.RecognitionAttemptKind;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+@Singleton
+@RequiredArgsConstructor(onConstructor_ = @Inject)
+public class RecognitionEnabledEligibilityRule implements RecognitionEligibilityRule {
+ private final Provider settingsProvider;
+ private final Provider providersProvider;
+
+ @Override
+ public @NotNull String id() {
+ return "recognition-enabled";
+ }
+
+ @Override
+ public int order() {
+ return 0;
+ }
+
+ @Override
+ public boolean supports(@NotNull RecognitionEligibilityContext context) {
+ return context.getAttemptKind() == RecognitionAttemptKind.SESSION_RECOGNITION;
+ }
+
+ @Override
+ public @NotNull RecognitionEligibilityRuleDecision evaluate(@NotNull RecognitionEligibilityContext context) {
+ String providerId = context.getProviderId();
+ if (providerId == null || providerId.isBlank())
+ return RecognitionEligibilityRuleDecision.block("recognition-provider-missing");
+ if (!isRecognitionEnabled(providerId))
+ return RecognitionEligibilityRuleDecision.block("recognition-disabled");
+
+ return RecognitionEligibilityRuleDecision.abstain();
+ }
+
+ private boolean isRecognitionEnabled(@NotNull String providerId) {
+ Providers.ProviderEntry provider = findProvider(providerId);
+ Providers.Overrides.Recognition overrides = provider != null ? provider.getOverrides().getRecognition() : null;
+ Boolean override = overrides != null ? overrides.getEnabled() : null;
+ if (override != null) return override;
+
+ return settingsProvider.get().getConnection().getSessions().getRecognition().isEnabled();
+ }
+
+ private @Nullable Providers.ProviderEntry findProvider(@Nullable String rawId) {
+ if (rawId == null || rawId.isBlank()) return null;
+
+ for (Providers.ProviderEntry provider : providersProvider.get().getProviders()) {
+ if (provider == null || provider.getId().isBlank()) continue;
+ if (provider.getId().trim().equalsIgnoreCase(rawId.trim())) return provider;
+ }
+ return null;
+ }
+}
diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/policy/UntrustedIpRecognitionGuard.java b/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/UntrustedIpRecognitionEligibilityRule.java
similarity index 61%
rename from identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/policy/UntrustedIpRecognitionGuard.java
rename to identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/UntrustedIpRecognitionEligibilityRule.java
index bc3d9b36c..8e138e76c 100644
--- a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/policy/UntrustedIpRecognitionGuard.java
+++ b/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/UntrustedIpRecognitionEligibilityRule.java
@@ -1,19 +1,17 @@
-package me.whereareiam.identica.common.identity.session.recognition.policy;
+package me.whereareiam.identica.common.identity.session.recognition.eligibility.rule;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import lombok.RequiredArgsConstructor;
-import me.whereareiam.identica.common.identity.session.recognition.policy.matcher.UntrustedIpMatcher;
-import me.whereareiam.identica.common.identity.session.recognition.policy.matcher.type.CidrMatcher;
-import me.whereareiam.identica.common.identity.session.recognition.policy.matcher.type.ExactIpMatcher;
-import me.whereareiam.identica.identity.session.recognition.policy.UntrustedIpRecognitionDecision;
-import me.whereareiam.identica.identity.session.recognition.policy.UntrustedIpRecognitionDecision.Outcome;
-import me.whereareiam.identica.identity.session.recognition.policy.UntrustedIpRecognitionPolicy;
+import me.whereareiam.identica.common.identity.session.recognition.eligibility.matcher.IpMatcher;
+import me.whereareiam.identica.common.identity.session.recognition.eligibility.matcher.type.CidrMatcher;
+import me.whereareiam.identica.common.identity.session.recognition.eligibility.matcher.type.ExactIpMatcher;
+import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityRule;
import me.whereareiam.identica.model.config.Providers;
import me.whereareiam.identica.model.config.Settings;
-import me.whereareiam.identica.model.provider.ProviderContext;
-import me.whereareiam.identica.type.provider.ProviderOrigin;
+import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityContext;
+import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityRuleDecision;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -23,42 +21,44 @@
@Singleton
@RequiredArgsConstructor(onConstructor_ = @Inject)
-public class UntrustedIpRecognitionGuard implements UntrustedIpRecognitionPolicy {
+public class UntrustedIpRecognitionEligibilityRule implements RecognitionEligibilityRule {
private final Provider settingsProvider;
private final Provider providersProvider;
@Override
- public @NotNull UntrustedIpRecognitionDecision evaluateAutomaticRecognition(
- @Nullable String providerId,
- @Nullable String clientIp,
- @Nullable ProviderContext selectedProvider
- ) {
- Settings.Sessions.Recognition.UntrustedIps untrustedIps = settingsProvider.get()
+ public @NotNull String id() {
+ return "untrusted-ip";
+ }
+
+ @Override
+ public int order() {
+ return 100;
+ }
+
+ @Override
+ public @NotNull RecognitionEligibilityRuleDecision evaluate(@NotNull RecognitionEligibilityContext context) {
+ Settings.Sessions.Recognition.Eligibility.UntrustedIps untrustedIps = settingsProvider.get()
.getConnection()
.getSessions()
.getRecognition()
+ .getEligibility()
.getUntrustedIps();
- if (!untrustedIps.isEnabled()) return decision(Outcome.ALLOWED_DISABLED);
- if (allowsRecognitionOverride(providerId)) return decision(Outcome.ALLOWED_PROVIDER_OVERRIDE);
- if (isExplicitSelection(selectedProvider, providerId))
- return decision(Outcome.ALLOWED_EXPLICIT_SELECTION);
+ if (!untrustedIps.isEnabled()) return RecognitionEligibilityRuleDecision.abstain();
+ if (allowsRecognitionOverride(context.getProviderId()))
+ return RecognitionEligibilityRuleDecision.allow("provider-override-untrusted-ip");
- InetAddress clientAddress = parseClientAddress(clientIp);
- if (clientAddress == null) return decision(Outcome.ALLOWED_MISSING_IP);
+ InetAddress clientAddress = parseClientAddress(context.getClientIp());
+ if (clientAddress == null) return RecognitionEligibilityRuleDecision.abstain();
List entries = untrustedIps.getEntries();
for (int i = 0; i < entries.size(); i++) {
- UntrustedIpMatcher matcher = parseEntry(entries.get(i), i);
- if (matcher.matches(clientAddress)) return decision(Outcome.BLOCKED_UNTRUSTED_IP);
+ IpMatcher matcher = parseEntry(entries.get(i), i);
+ if (matcher.matches(clientAddress))
+ return RecognitionEligibilityRuleDecision.block("blocked-untrusted-ip");
}
- return decision(Outcome.ALLOWED_IP_NOT_MATCHED);
- }
-
- @Override
- public boolean isUntrustedIp(@Nullable String clientIp) {
- return evaluateAutomaticRecognition(null, clientIp, null).isBlocked();
+ return RecognitionEligibilityRuleDecision.abstain();
}
private boolean allowsRecognitionOverride(@Nullable String providerId) {
@@ -67,26 +67,13 @@ private boolean allowsRecognitionOverride(@Nullable String providerId) {
for (Providers.ProviderEntry entry : providersProvider.get().getProviders()) {
if (entry == null) continue;
if (!entry.getId().equalsIgnoreCase(providerId)) continue;
- return entry.getOverrides().isAllowRecognitionOnUntrustedIp();
+ return entry.getOverrides().getRecognition().getEligibility().isAllowOnUntrustedIp();
}
return false;
}
- private boolean isExplicitSelection(@Nullable ProviderContext selectedProvider, @Nullable String providerId) {
- if (selectedProvider == null || providerId == null || providerId.isBlank()) return false;
- if (selectedProvider.getProviderId() == null || !selectedProvider.getProviderId().equalsIgnoreCase(providerId))
- return false;
-
- ProviderOrigin source = selectedProvider.getSource();
- return source == ProviderOrigin.ENTRYPOINT || source == ProviderOrigin.MANUAL;
- }
-
- private @NotNull UntrustedIpRecognitionDecision decision(@NotNull Outcome outcome) {
- return new UntrustedIpRecognitionDecision(outcome);
- }
-
- private @NotNull UntrustedIpMatcher parseEntry(@Nullable String entry, int index) {
+ private @NotNull IpMatcher parseEntry(@Nullable String entry, int index) {
if (entry == null || entry.isBlank()) throw invalidEntry(index, entry, "entry must not be blank");
String trimmed = entry.trim();
@@ -96,7 +83,7 @@ private boolean isExplicitSelection(@Nullable ProviderContext selectedProvider,
return new ExactIpMatcher(literal);
}
- private @NotNull UntrustedIpMatcher parseCidr(@NotNull String entry, int index) {
+ private @NotNull IpMatcher parseCidr(@NotNull String entry, int index) {
String[] parts = entry.split("/", -1);
if (parts.length != 2) throw invalidEntry(index, entry, "CIDR entry must contain exactly one '/' separator");
@@ -173,7 +160,7 @@ private boolean isHexDigit(int character) {
private @NotNull IllegalStateException invalidEntry(int index, @Nullable String entry, @NotNull String reason) {
return new IllegalStateException(
- "settings.connection.sessions.recognition.untrustedIps.entries[" + index + "]="
+ "settings.connection.sessions.recognition.eligibility.untrustedIps.entries[" + index + "]="
+ entry
+ " is invalid: "
+ reason
diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/policy/matcher/type/CidrMatcher.java b/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/policy/matcher/type/CidrMatcher.java
deleted file mode 100644
index 45bebced8..000000000
--- a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/policy/matcher/type/CidrMatcher.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package me.whereareiam.identica.common.identity.session.recognition.policy.matcher.type;
-
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-import lombok.ToString;
-import me.whereareiam.identica.common.identity.session.recognition.policy.matcher.UntrustedIpMatcher;
-import org.jetbrains.annotations.NotNull;
-
-import java.net.InetAddress;
-
-@Getter
-@ToString
-@AllArgsConstructor
-public final class CidrMatcher implements UntrustedIpMatcher {
- private final @NotNull InetAddress base;
- private final int prefix;
-
- @Override
- public boolean matches(@NotNull InetAddress address) {
- byte[] left = base.getAddress();
- byte[] right = address.getAddress();
- if (left.length != right.length)
- return false;
-
- int remaining = prefix;
- for (int i = 0; i < left.length; i++) {
- if (remaining <= 0)
- return true;
-
- int bits = Math.min(remaining, 8);
- int mask = bits == 8 ? 0xFF : (0xFF << (8 - bits)) & 0xFF;
- if ((left[i] & mask) != (right[i] & mask))
- return false;
-
- remaining -= bits;
- }
-
- return true;
- }
-}
diff --git a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/policy/matcher/type/ExactIpMatcher.java b/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/policy/matcher/type/ExactIpMatcher.java
deleted file mode 100644
index e3b418989..000000000
--- a/identica-common/src/main/java/me/whereareiam/identica/common/identity/session/recognition/policy/matcher/type/ExactIpMatcher.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package me.whereareiam.identica.common.identity.session.recognition.policy.matcher.type;
-
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-import lombok.ToString;
-import me.whereareiam.identica.common.identity.session.recognition.policy.matcher.UntrustedIpMatcher;
-import org.jetbrains.annotations.NotNull;
-
-import java.net.InetAddress;
-
-@Getter
-@ToString
-@AllArgsConstructor
-public final class ExactIpMatcher implements UntrustedIpMatcher {
- private final @NotNull InetAddress expected;
-
- @Override
- public boolean matches(@NotNull InetAddress address) {
- return expected.equals(address);
- }
-}
diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/config/defaults/ProvidersDefaultsTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/config/defaults/ProvidersDefaultsTest.java
index bfe68ffd1..70e2a25ee 100644
--- a/identica-common/src/test/java/me/whereareiam/identica/common/config/defaults/ProvidersDefaultsTest.java
+++ b/identica-common/src/test/java/me/whereareiam/identica/common/config/defaults/ProvidersDefaultsTest.java
@@ -13,12 +13,12 @@ class ProvidersDefaultsTest {
void providerRecognitionOverridesDefaultToDisabled() {
Providers providers = new ProvidersDefaults().supply(new Providers());
- assertFalse(provider(providers, "premium").getOverrides().isAllowRecognitionOnUntrustedIp());
assertTrue(provider(providers, "premium").getOverrides().getRecognition().getSignals().isEmpty());
assertNull(provider(providers, "premium").getOverrides().getRecognition().getEnabled());
- assertFalse(provider(providers, "credential").getOverrides().isAllowRecognitionOnUntrustedIp());
+ assertFalse(provider(providers, "premium").getOverrides().getRecognition().getEligibility().isAllowOnUntrustedIp());
assertTrue(provider(providers, "credential").getOverrides().getRecognition().getSignals().isEmpty());
assertNull(provider(providers, "credential").getOverrides().getRecognition().getEnabled());
+ assertFalse(provider(providers, "credential").getOverrides().getRecognition().getEligibility().isAllowOnUntrustedIp());
}
private Providers.ProviderEntry provider(Providers providers, String id) {
diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/config/defaults/SettingsDefaultsTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/config/defaults/SettingsDefaultsTest.java
index e53065048..fb6c43e81 100644
--- a/identica-common/src/test/java/me/whereareiam/identica/common/config/defaults/SettingsDefaultsTest.java
+++ b/identica-common/src/test/java/me/whereareiam/identica/common/config/defaults/SettingsDefaultsTest.java
@@ -7,7 +7,7 @@
import me.whereareiam.identica.model.config.Settings;
import me.whereareiam.identica.type.PlatformType;
import me.whereareiam.identica.type.event.EventPriority;
-import me.whereareiam.identica.type.session.RecognitionSignal;
+import me.whereareiam.identica.type.session.recognition.RecognitionSignal;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -35,6 +35,7 @@ void generatedRoutingScenariosAreEmptyByDefault() {
java.util.List.of(RecognitionSignal.USERNAME, RecognitionSignal.IP, RecognitionSignal.VIRTUAL_HOST),
settings.getConnection().getSessions().getRecognition().getDefaultSignals()
);
+ assertTrue(settings.getConnection().getSessions().getRecognition().getEligibility().getUntrustedIps().isEnabled());
assertEquals(java.time.Duration.ofHours(12), settings.getConnection().getSessions().getActiveTtl());
assertEquals(java.time.Duration.ofHours(12), settings.getConnection().getSessions().getRecognition().getValidity());
}
diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/DefaultSessionRecognitionServiceTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/DefaultSessionRecognitionServiceTest.java
index ccaaa2730..8a19b4de7 100644
--- a/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/DefaultSessionRecognitionServiceTest.java
+++ b/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/DefaultSessionRecognitionServiceTest.java
@@ -1,12 +1,15 @@
package me.whereareiam.identica.common.identity.session.recognition;
-import me.whereareiam.identica.common.identity.session.recognition.policy.UntrustedIpRecognitionGuard;
+import me.whereareiam.identica.common.identity.session.recognition.eligibility.DefaultRecognitionEligibilityRegistry;
+import me.whereareiam.identica.common.identity.session.recognition.eligibility.DefaultRecognitionEligibilityService;
+import me.whereareiam.identica.common.identity.session.recognition.eligibility.rule.RecognitionEnabledEligibilityRule;
+import me.whereareiam.identica.common.identity.session.recognition.eligibility.rule.UntrustedIpRecognitionEligibilityRule;
import me.whereareiam.identica.identity.actor.ConnectionIdentity;
import me.whereareiam.identica.identity.session.recognition.SessionRecognitionStore;
import me.whereareiam.identica.model.config.Providers;
import me.whereareiam.identica.model.config.Settings;
import me.whereareiam.identica.model.session.SessionRecognitionSnapshot;
-import me.whereareiam.identica.type.session.RecognitionSignal;
+import me.whereareiam.identica.type.session.recognition.RecognitionSignal;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -71,11 +74,15 @@ private DefaultSessionRecognitionService service(
Providers providers,
SessionRecognitionStore store
) {
+ DefaultRecognitionEligibilityRegistry registry = new DefaultRecognitionEligibilityRegistry();
+ registry.register(new RecognitionEnabledEligibilityRule(() -> settings, () -> providers));
+ registry.register(new UntrustedIpRecognitionEligibilityRule(() -> settings, () -> providers));
+
return new DefaultSessionRecognitionService(
() -> settings,
() -> providers,
store,
- new UntrustedIpRecognitionGuard(() -> settings, () -> providers)
+ new DefaultRecognitionEligibilityService(registry)
);
}
@@ -89,15 +96,19 @@ private Settings settings(
List signals,
List untrustedIps
) {
- Settings.Sessions.Recognition.UntrustedIps untrusted = new Settings.Sessions.Recognition.UntrustedIps();
+ Settings.Sessions.Recognition.Eligibility.UntrustedIps untrusted =
+ new Settings.Sessions.Recognition.Eligibility.UntrustedIps();
untrusted.setEnabled(true);
untrusted.setEntries(untrustedIps);
+ Settings.Sessions.Recognition.Eligibility eligibility = new Settings.Sessions.Recognition.Eligibility();
+ eligibility.setUntrustedIps(untrusted);
+
Settings.Sessions.Recognition recognition = new Settings.Sessions.Recognition();
recognition.setEnabled(true);
recognition.setValidity(Duration.ofHours(12));
recognition.setDefaultSignals(signals);
- recognition.setUntrustedIps(untrusted);
+ recognition.setEligibility(eligibility);
Settings.Sessions sessions = new Settings.Sessions();
sessions.setRecognition(recognition);
@@ -113,7 +124,7 @@ private Settings settings(
private Providers providers(boolean allowRecognitionOnUntrustedIp) {
Providers.ProviderEntry provider = new Providers.ProviderEntry();
provider.setId("credential");
- provider.getOverrides().setAllowRecognitionOnUntrustedIp(allowRecognitionOnUntrustedIp);
+ provider.getOverrides().getRecognition().getEligibility().setAllowOnUntrustedIp(allowRecognitionOnUntrustedIp);
Providers providers = new Providers();
providers.setProviders(List.of(provider));
diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/DefaultRecognitionEligibilityRegistryTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/DefaultRecognitionEligibilityRegistryTest.java
new file mode 100644
index 000000000..aaccb3f8f
--- /dev/null
+++ b/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/DefaultRecognitionEligibilityRegistryTest.java
@@ -0,0 +1,55 @@
+package me.whereareiam.identica.common.identity.session.recognition.eligibility;
+
+import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityRule;
+import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityContext;
+import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityRuleDecision;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("Default Recognition Eligibility Registry")
+class DefaultRecognitionEligibilityRegistryTest {
+ @DisplayName("Registers, replaces, and unregisters rules by id")
+ @Test
+ void registersReplacesAndUnregistersRulesById() {
+ DefaultRecognitionEligibilityRegistry registry = new DefaultRecognitionEligibilityRegistry();
+
+ registry.register(rule("corporate", 10));
+ registry.register(rule("Corporate", 20));
+
+ assertEquals(1, registry.resolve().size());
+ assertEquals(20, registry.resolve().getFirst().order());
+ assertTrue(registry.unregister("CORPORATE"));
+ assertTrue(registry.resolve().isEmpty());
+ }
+
+ @DisplayName("Resolved rule list is immutable")
+ @Test
+ void resolvedRuleListIsImmutable() {
+ DefaultRecognitionEligibilityRegistry registry = new DefaultRecognitionEligibilityRegistry();
+ registry.register(rule("rule-1", 10));
+
+ assertThrows(UnsupportedOperationException.class, () -> registry.resolve().add(rule("rule-2", 20)));
+ }
+
+ private @NotNull RecognitionEligibilityRule rule(@NotNull String id, int order) {
+ return new RecognitionEligibilityRule() {
+ @Override
+ public @NotNull String id() {
+ return id;
+ }
+
+ @Override
+ public int order() {
+ return order;
+ }
+
+ @Override
+ public @NotNull RecognitionEligibilityRuleDecision evaluate(@NotNull RecognitionEligibilityContext context) {
+ return RecognitionEligibilityRuleDecision.abstain();
+ }
+ };
+ }
+}
diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/DefaultRecognitionEligibilityServiceTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/DefaultRecognitionEligibilityServiceTest.java
new file mode 100644
index 000000000..27f68a14b
--- /dev/null
+++ b/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/DefaultRecognitionEligibilityServiceTest.java
@@ -0,0 +1,104 @@
+package me.whereareiam.identica.common.identity.session.recognition.eligibility;
+
+import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityRule;
+import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityContext;
+import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityDecision;
+import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityRuleDecision;
+import me.whereareiam.identica.type.session.recognition.RecognitionAttemptKind;
+import me.whereareiam.identica.type.session.recognition.RecognitionTrigger;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("Default Recognition Eligibility Service")
+class DefaultRecognitionEligibilityServiceTest {
+ @DisplayName("All abstain leaves recognition eligible")
+ @Test
+ void allAbstainLeavesRecognitionEligible() {
+ DefaultRecognitionEligibilityRegistry registry = new DefaultRecognitionEligibilityRegistry();
+ registry.register(rule("a", 10, RecognitionEligibilityRuleDecision.abstain(), true));
+
+ RecognitionEligibilityDecision decision = new DefaultRecognitionEligibilityService(registry).evaluate(context());
+
+ assertTrue(decision.isAllowed());
+ assertEquals("no-rule-blocked", decision.getReason());
+ assertNull(decision.getRuleId());
+ }
+
+ @DisplayName("Later block overrides earlier allow")
+ @Test
+ void laterBlockOverridesEarlierAllow() {
+ DefaultRecognitionEligibilityRegistry registry = new DefaultRecognitionEligibilityRegistry();
+ registry.register(rule("allow", 10, RecognitionEligibilityRuleDecision.allow("allowed"), true));
+ registry.register(rule("block", 20, RecognitionEligibilityRuleDecision.block("blocked"), true));
+
+ RecognitionEligibilityDecision decision = new DefaultRecognitionEligibilityService(registry).evaluate(context());
+
+ assertFalse(decision.isAllowed());
+ assertEquals("blocked", decision.getReason());
+ assertEquals("block", decision.getRuleId());
+ }
+
+ @DisplayName("Supports false skips a rule")
+ @Test
+ void supportsFalseSkipsRule() {
+ DefaultRecognitionEligibilityRegistry registry = new DefaultRecognitionEligibilityRegistry();
+ registry.register(rule("block", 10, RecognitionEligibilityRuleDecision.block("blocked"), false));
+
+ RecognitionEligibilityDecision decision = new DefaultRecognitionEligibilityService(registry).evaluate(context());
+
+ assertTrue(decision.isAllowed());
+ }
+
+ @DisplayName("Order ties are resolved by normalized id")
+ @Test
+ void orderTiesAreResolvedByNormalizedId() {
+ DefaultRecognitionEligibilityRegistry registry = new DefaultRecognitionEligibilityRegistry();
+ registry.register(rule("z-rule", 10, RecognitionEligibilityRuleDecision.allow("z"), true));
+ registry.register(rule("a-rule", 10, RecognitionEligibilityRuleDecision.block("a"), true));
+
+ RecognitionEligibilityDecision decision = new DefaultRecognitionEligibilityService(registry).evaluate(context());
+
+ assertFalse(decision.isAllowed());
+ assertEquals("a-rule", decision.getRuleId());
+ }
+
+ private @NotNull RecognitionEligibilityContext context() {
+ return RecognitionEligibilityContext.builder()
+ .providerId("premium")
+ .attemptKind(RecognitionAttemptKind.SESSION_RECOGNITION)
+ .trigger(RecognitionTrigger.AUTOMATIC)
+ .build();
+ }
+
+ private @NotNull RecognitionEligibilityRule rule(
+ @NotNull String id,
+ int order,
+ @NotNull RecognitionEligibilityRuleDecision decision,
+ boolean supported
+ ) {
+ return new RecognitionEligibilityRule() {
+ @Override
+ public @NotNull String id() {
+ return id;
+ }
+
+ @Override
+ public int order() {
+ return order;
+ }
+
+ @Override
+ public boolean supports(@NotNull RecognitionEligibilityContext context) {
+ return supported;
+ }
+
+ @Override
+ public @NotNull RecognitionEligibilityRuleDecision evaluate(@NotNull RecognitionEligibilityContext context) {
+ return decision;
+ }
+ };
+ }
+}
diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/ExplicitSelectionRecognitionEligibilityRuleTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/ExplicitSelectionRecognitionEligibilityRuleTest.java
new file mode 100644
index 000000000..e52b4d381
--- /dev/null
+++ b/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/ExplicitSelectionRecognitionEligibilityRuleTest.java
@@ -0,0 +1,45 @@
+package me.whereareiam.identica.common.identity.session.recognition.eligibility.rule;
+
+import me.whereareiam.identica.model.provider.ProviderContext;
+import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityContext;
+import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityRuleDecision;
+import me.whereareiam.identica.type.provider.ProviderOrigin;
+import me.whereareiam.identica.type.session.recognition.RecognitionAttemptKind;
+import me.whereareiam.identica.type.session.recognition.RecognitionTrigger;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@DisplayName("Explicit Selection Recognition Eligibility Rule")
+class ExplicitSelectionRecognitionEligibilityRuleTest {
+ @DisplayName("Allows explicit manual provider selection")
+ @Test
+ void allowsExplicitManualProviderSelection() {
+ RecognitionEligibilityRuleDecision decision = new ExplicitSelectionRecognitionEligibilityRule().evaluate(
+ RecognitionEligibilityContext.builder()
+ .providerId("premium")
+ .selectedProvider(ProviderContext.of("premium", null, "PlayerOne", ProviderOrigin.MANUAL))
+ .attemptKind(RecognitionAttemptKind.PROVIDER_HANDSHAKE_RECOGNITION)
+ .trigger(RecognitionTrigger.EXPLICIT_PROVIDER_SELECTION)
+ .build()
+ );
+
+ assertEquals(RecognitionEligibilityRuleDecision.Status.ALLOW, decision.getStatus());
+ }
+
+ @DisplayName("Abstains for automatic provider selection")
+ @Test
+ void abstainsForAutomaticProviderSelection() {
+ RecognitionEligibilityRuleDecision decision = new ExplicitSelectionRecognitionEligibilityRule().evaluate(
+ RecognitionEligibilityContext.builder()
+ .providerId("premium")
+ .selectedProvider(ProviderContext.of("premium", null, "PlayerOne", ProviderOrigin.AUTO))
+ .attemptKind(RecognitionAttemptKind.PROVIDER_HANDSHAKE_RECOGNITION)
+ .trigger(RecognitionTrigger.AUTOMATIC)
+ .build()
+ );
+
+ assertEquals(RecognitionEligibilityRuleDecision.Status.ABSTAIN, decision.getStatus());
+ }
+}
diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/RecognitionEnabledEligibilityRuleTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/RecognitionEnabledEligibilityRuleTest.java
new file mode 100644
index 000000000..f3ee54a85
--- /dev/null
+++ b/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/RecognitionEnabledEligibilityRuleTest.java
@@ -0,0 +1,76 @@
+package me.whereareiam.identica.common.identity.session.recognition.eligibility.rule;
+
+import me.whereareiam.identica.model.config.Providers;
+import me.whereareiam.identica.model.config.Settings;
+import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityContext;
+import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityRuleDecision;
+import me.whereareiam.identica.type.session.recognition.RecognitionAttemptKind;
+import me.whereareiam.identica.type.session.recognition.RecognitionTrigger;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.time.Duration;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@DisplayName("Recognition Enabled Eligibility Rule")
+class RecognitionEnabledEligibilityRuleTest {
+ @DisplayName("Blocks session recognition when recognition is disabled")
+ @Test
+ void blocksSessionRecognitionWhenRecognitionIsDisabled() {
+ Settings settings = settings(false);
+ Providers providers = providers(null);
+
+ RecognitionEligibilityRuleDecision decision = new RecognitionEnabledEligibilityRule(() -> settings, () -> providers)
+ .evaluate(context(RecognitionAttemptKind.SESSION_RECOGNITION));
+
+ assertEquals(RecognitionEligibilityRuleDecision.Status.BLOCK, decision.getStatus());
+ }
+
+ @DisplayName("Provider override restores recognition when enabled")
+ @Test
+ void providerOverrideRestoresRecognitionWhenEnabled() {
+ Settings settings = settings(false);
+ Providers providers = providers(true);
+
+ RecognitionEligibilityRuleDecision decision = new RecognitionEnabledEligibilityRule(() -> settings, () -> providers)
+ .evaluate(context(RecognitionAttemptKind.SESSION_RECOGNITION));
+
+ assertEquals(RecognitionEligibilityRuleDecision.Status.ABSTAIN, decision.getStatus());
+ }
+
+ private RecognitionEligibilityContext context(RecognitionAttemptKind attemptKind) {
+ return RecognitionEligibilityContext.builder()
+ .providerId("premium")
+ .attemptKind(attemptKind)
+ .trigger(RecognitionTrigger.AUTOMATIC)
+ .build();
+ }
+
+ private Settings settings(boolean enabled) {
+ Settings.Sessions.Recognition recognition = new Settings.Sessions.Recognition();
+ recognition.setEnabled(enabled);
+ recognition.setValidity(Duration.ofHours(12));
+
+ Settings.Sessions sessions = new Settings.Sessions();
+ sessions.setRecognition(recognition);
+
+ Settings.Connection connection = new Settings.Connection();
+ connection.setSessions(sessions);
+
+ Settings settings = new Settings();
+ settings.setConnection(connection);
+ return settings;
+ }
+
+ private Providers providers(Boolean enabledOverride) {
+ Providers.ProviderEntry provider = new Providers.ProviderEntry();
+ provider.setId("premium");
+ provider.getOverrides().getRecognition().setEnabled(enabledOverride);
+
+ Providers providers = new Providers();
+ providers.setProviders(List.of(provider));
+ return providers;
+ }
+}
diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/UntrustedIpRecognitionEligibilityRuleTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/UntrustedIpRecognitionEligibilityRuleTest.java
new file mode 100644
index 000000000..459e26639
--- /dev/null
+++ b/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/eligibility/rule/UntrustedIpRecognitionEligibilityRuleTest.java
@@ -0,0 +1,92 @@
+package me.whereareiam.identica.common.identity.session.recognition.eligibility.rule;
+
+import me.whereareiam.identica.model.config.Providers;
+import me.whereareiam.identica.model.config.Settings;
+import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityContext;
+import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityRuleDecision;
+import me.whereareiam.identica.type.session.recognition.RecognitionAttemptKind;
+import me.whereareiam.identica.type.session.recognition.RecognitionTrigger;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("Untrusted IP Recognition Eligibility Rule")
+class UntrustedIpRecognitionEligibilityRuleTest {
+ @DisplayName("Matches exact IPv4 entries")
+ @Test
+ void matchesExactIpv4Entries() {
+ RecognitionEligibilityRuleDecision decision = rule(settings(true, List.of("203.0.113.10")), providers(false))
+ .evaluate(context("203.0.113.10"));
+
+ assertEquals(RecognitionEligibilityRuleDecision.Status.BLOCK, decision.getStatus());
+ }
+
+ @DisplayName("Provider override allows recognition on untrusted IPs")
+ @Test
+ void providerOverrideAllowsRecognitionOnUntrustedIps() {
+ RecognitionEligibilityRuleDecision decision = rule(settings(true, List.of("203.0.113.10")), providers(true))
+ .evaluate(context("203.0.113.10"));
+
+ assertEquals(RecognitionEligibilityRuleDecision.Status.ALLOW, decision.getStatus());
+ }
+
+ @DisplayName("Invalid entries fail with the eligibility config path")
+ @Test
+ void invalidEntriesFailWithEligibilityConfigPath() {
+ IllegalStateException exception = assertThrows(
+ IllegalStateException.class,
+ () -> rule(settings(true, List.of("not-an-ip")), providers(false)).evaluate(context("203.0.113.10"))
+ );
+
+ assertTrue(exception.getMessage().contains("settings.connection.sessions.recognition.eligibility.untrustedIps.entries[0]"));
+ }
+
+ private UntrustedIpRecognitionEligibilityRule rule(Settings settings, Providers providers) {
+ return new UntrustedIpRecognitionEligibilityRule(() -> settings, () -> providers);
+ }
+
+ private RecognitionEligibilityContext context(String ip) {
+ return RecognitionEligibilityContext.builder()
+ .providerId("premium")
+ .clientIp(ip)
+ .attemptKind(RecognitionAttemptKind.SESSION_RECOGNITION)
+ .trigger(RecognitionTrigger.AUTOMATIC)
+ .build();
+ }
+
+ private Settings settings(boolean enabled, List entries) {
+ Settings.Sessions.Recognition.Eligibility.UntrustedIps untrustedIps =
+ new Settings.Sessions.Recognition.Eligibility.UntrustedIps();
+ untrustedIps.setEnabled(enabled);
+ untrustedIps.setEntries(entries);
+
+ Settings.Sessions.Recognition.Eligibility eligibility = new Settings.Sessions.Recognition.Eligibility();
+ eligibility.setUntrustedIps(untrustedIps);
+
+ Settings.Sessions.Recognition recognition = new Settings.Sessions.Recognition();
+ recognition.setEligibility(eligibility);
+
+ Settings.Sessions sessions = new Settings.Sessions();
+ sessions.setRecognition(recognition);
+
+ Settings.Connection connection = new Settings.Connection();
+ connection.setSessions(sessions);
+
+ Settings settings = new Settings();
+ settings.setConnection(connection);
+ return settings;
+ }
+
+ private Providers providers(boolean allowOnUntrustedIp) {
+ Providers.ProviderEntry premium = new Providers.ProviderEntry();
+ premium.setId("premium");
+ premium.getOverrides().getRecognition().getEligibility().setAllowOnUntrustedIp(allowOnUntrustedIp);
+
+ Providers providers = new Providers();
+ providers.setProviders(List.of(premium));
+ return providers;
+ }
+}
diff --git a/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/policy/UntrustedIpRecognitionGuardTest.java b/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/policy/UntrustedIpRecognitionGuardTest.java
deleted file mode 100644
index 542d7a250..000000000
--- a/identica-common/src/test/java/me/whereareiam/identica/common/identity/session/recognition/policy/UntrustedIpRecognitionGuardTest.java
+++ /dev/null
@@ -1,144 +0,0 @@
-package me.whereareiam.identica.common.identity.session.recognition.policy;
-
-import me.whereareiam.identica.identity.session.recognition.policy.UntrustedIpRecognitionDecision.Outcome;
-import me.whereareiam.identica.model.config.Providers;
-import me.whereareiam.identica.model.config.Settings;
-import me.whereareiam.identica.model.provider.ProviderContext;
-import me.whereareiam.identica.type.provider.ProviderOrigin;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
-
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-@DisplayName("Untrusted IP Recognition Guard")
-class UntrustedIpRecognitionGuardTest {
- @DisplayName("Matches an exact IPv4 entry")
- @Test
- void matchesExactIpv4Entry() {
- assertTrue(guard(settings(true, List.of("203.0.113.10")), providers())
- .isUntrustedIp("203.0.113.10"));
- }
-
- @DisplayName("Matches an IPv4 CIDR entry")
- @Test
- void matchesIpv4CidrEntry() {
- assertTrue(guard(settings(true, List.of("203.0.113.0/24")), providers())
- .isUntrustedIp("203.0.113.77"));
- }
-
- @DisplayName("Matches an IPv6 CIDR entry")
- @Test
- void matchesIpv6CidrEntry() {
- assertTrue(guard(settings(true, List.of("2001:db8::/32")), providers())
- .isUntrustedIp("2001:db8::42"));
- }
-
- @DisplayName("Does not block when the feature is disabled")
- @Test
- void disabledGuardDoesNotBlockRecognition() {
- assertFalse(guard(settings(false, List.of("203.0.113.10")), providers())
- .evaluateAutomaticRecognition("premium", "203.0.113.10", null)
- .isBlocked());
- }
-
- @DisplayName("Does not block when the client IP is missing")
- @Test
- void missingIpDoesNotBlockRecognition() {
- assertFalse(guard(settings(true, List.of("203.0.113.10")), providers())
- .evaluateAutomaticRecognition("premium", null, null)
- .isBlocked());
- }
-
- @DisplayName("Fails clearly when a configured entry is malformed")
- @Test
- void invalidEntryFailsClearly() {
- IllegalStateException exception = assertThrows(
- IllegalStateException.class,
- () -> guard(settings(true, List.of("not-an-ip")), providers()).isUntrustedIp("203.0.113.10")
- );
-
- assertTrue(exception.getMessage().contains("settings.connection.sessions.recognition.untrustedIps.entries[0]"));
- }
-
- @DisplayName("Provider override restores automatic recognition")
- @Test
- void providerOverrideRestoresRecognition() {
- Providers providers = providers();
- provider(providers).getOverrides().setAllowRecognitionOnUntrustedIp(true);
-
- assertSame(Outcome.ALLOWED_PROVIDER_OVERRIDE, guard(settings(true, List.of("203.0.113.10")), providers)
- .evaluateAutomaticRecognition("premium", "203.0.113.10", null)
- .getOutcome());
- }
-
- @DisplayName("Explicit entrypoint selection bypasses blocking")
- @Test
- void explicitEntrypointSelectionBypassesBlocking() {
- assertSame(Outcome.ALLOWED_EXPLICIT_SELECTION, guard(settings(true, List.of("203.0.113.10")), providers())
- .evaluateAutomaticRecognition(
- "premium",
- "203.0.113.10",
- ProviderContext.of("premium", null, "PlayerOne", ProviderOrigin.ENTRYPOINT)
- ).getOutcome());
- }
-
- @DisplayName("Explicit manual selection bypasses blocking")
- @Test
- void explicitManualSelectionBypassesBlocking() {
- assertSame(Outcome.ALLOWED_EXPLICIT_SELECTION, guard(settings(true, List.of("203.0.113.10")), providers())
- .evaluateAutomaticRecognition(
- "premium",
- "203.0.113.10",
- ProviderContext.of("premium", null, "PlayerOne", ProviderOrigin.MANUAL)
- ).getOutcome());
- }
-
- @DisplayName("Valid configuration can be resolved eagerly without throwing")
- @Test
- void validConfigurationResolvesEagerly() {
- assertDoesNotThrow(() -> guard(settings(true, List.of("203.0.113.0/24")), providers())
- .isUntrustedIp("203.0.113.42"));
- }
-
- private UntrustedIpRecognitionGuard guard(Settings settings, Providers providers) {
- return new UntrustedIpRecognitionGuard(() -> settings, () -> providers);
- }
-
- private Settings settings(boolean enabled, List entries) {
- Settings.Sessions.Recognition.UntrustedIps untrustedIps = new Settings.Sessions.Recognition.UntrustedIps();
- untrustedIps.setEnabled(enabled);
- untrustedIps.setEntries(entries);
-
- Settings.Sessions.Recognition recognition = new Settings.Sessions.Recognition();
- recognition.setUntrustedIps(untrustedIps);
-
- Settings.Sessions sessions = new Settings.Sessions();
- sessions.setRecognition(recognition);
-
- Settings.Connection connection = new Settings.Connection();
- connection.setSessions(sessions);
-
- Settings settings = new Settings();
- settings.setConnection(connection);
- return settings;
- }
-
- private Providers providers() {
- Providers providers = new Providers();
- Providers.ProviderEntry premium = new Providers.ProviderEntry();
- premium.setId("premium");
- Providers.ProviderEntry credential = new Providers.ProviderEntry();
- credential.setId("credential");
- providers.setProviders(List.of(premium, credential));
- return providers;
- }
-
- private Providers.ProviderEntry provider(Providers providers) {
- return providers.getProviders().stream()
- .filter(entry -> entry.getId().equals("premium"))
- .findFirst()
- .orElseThrow();
- }
-}
diff --git a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/handshake/phase/EvaluateHandshakePhase.java b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/handshake/phase/EvaluateHandshakePhase.java
index a6dc466d8..dda3e8c3d 100644
--- a/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/handshake/phase/EvaluateHandshakePhase.java
+++ b/identica-engine/src/main/java/me/whereareiam/identica/engine/pipeline/prepare/group/handshake/phase/EvaluateHandshakePhase.java
@@ -7,8 +7,7 @@
import me.whereareiam.identica.handshake.HandshakeStore;
import me.whereareiam.identica.handshake.policy.HandshakePolicy;
import me.whereareiam.identica.handshake.policy.ProviderScopedHandshakePolicy;
-import me.whereareiam.identica.identity.session.recognition.policy.UntrustedIpRecognitionDecision;
-import me.whereareiam.identica.identity.session.recognition.policy.UntrustedIpRecognitionPolicy;
+import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityService;
import me.whereareiam.identica.logging.Logger;
import me.whereareiam.identica.model.auth.handshake.HandshakeDecision;
import me.whereareiam.identica.model.auth.handshake.HandshakeRequest;
@@ -17,7 +16,12 @@
import me.whereareiam.identica.model.pipeline.prepare.decision.PrepareDecisionItem;
import me.whereareiam.identica.model.pipeline.state.PipelineState;
import me.whereareiam.identica.model.provider.ProviderContext;
+import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityContext;
+import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityDecision;
import me.whereareiam.identica.pipeline.PipelinePhase;
+import me.whereareiam.identica.type.provider.ProviderOrigin;
+import me.whereareiam.identica.type.session.recognition.RecognitionAttemptKind;
+import me.whereareiam.identica.type.session.recognition.RecognitionTrigger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -28,7 +32,7 @@
@RequiredArgsConstructor(onConstructor_ = @Inject)
public class EvaluateHandshakePhase implements PipelinePhase {
private final HandshakeStore handshakeStore;
- private final UntrustedIpRecognitionPolicy untrustedIpRecognitionPolicy;
+ private final RecognitionEligibilityService recognitionEligibilityService;
@Override
public @NotNull String id() {
@@ -73,7 +77,8 @@ public boolean supports(@NotNull PipelineState pipelineState, @NotNull PrepareGr
HandshakeDecision decision = HandshakeDecision.allow();
String clientIp = state.getRequest().getIdentity().getIp();
for (HandshakePolicy policy : handshakeStore.policies()) {
- if (!shouldEvaluate(policy, context.getProvider(), clientIp)) continue;
+ if (!shouldEvaluate(policy, context.getProvider(), clientIp, state.getRequest().getIdentity().getOrigin()))
+ continue;
decision = merge(decision, evaluatePolicy(policy, request));
}
@@ -85,27 +90,45 @@ public boolean supports(@NotNull PipelineState pipelineState, @NotNull PrepareGr
private boolean shouldEvaluate(
@NotNull HandshakePolicy policy,
@Nullable ProviderContext provider,
- @Nullable String clientIp
+ @Nullable String clientIp,
+ @Nullable me.whereareiam.identica.identity.actor.ConnectionIdentity.Origin origin
) {
if (!(policy instanceof ProviderScopedHandshakePolicy scoped)) return true;
if (provider == null || provider.getProviderId() == null || provider.getProviderId().isBlank()) return true;
- UntrustedIpRecognitionDecision decision = untrustedIpRecognitionPolicy.evaluateAutomaticRecognition(
- scoped.providerId(),
- clientIp,
- provider
- );
- if (decision.isBlocked()) {
+ if (!scoped.providerId().equalsIgnoreCase(provider.getProviderId()))
+ return false;
+
+ RecognitionEligibilityDecision decision = recognitionEligibilityService.evaluate(RecognitionEligibilityContext.builder()
+ .providerId(scoped.providerId())
+ .providerUsername(provider.getProviderUsername())
+ .clientIp(clientIp)
+ .selectedProvider(provider)
+ .origin(origin)
+ .attemptKind(RecognitionAttemptKind.PROVIDER_HANDSHAKE_RECOGNITION)
+ .trigger(resolveTrigger(provider))
+ .build());
+
+ if (!decision.isAllowed()) {
Logger.debug(
- "Skipping provider-scoped handshake recognition provider=%s username=%s ip=%s outcome=%s",
+ "Skipping provider-scoped handshake recognition provider=%s username=%s ip=%s rule=%s reason=%s",
scoped.providerId(),
provider.getProviderUsername(),
clientIp,
- decision.getOutcome()
+ decision.getRuleId(),
+ decision.getReason()
);
return false;
}
- return scoped.providerId().equalsIgnoreCase(provider.getProviderId());
+ return true;
+ }
+
+ private @NotNull RecognitionTrigger resolveTrigger(@NotNull ProviderContext provider) {
+ ProviderOrigin source = provider.getSource();
+ if (source == ProviderOrigin.ENTRYPOINT || source == ProviderOrigin.MANUAL)
+ return RecognitionTrigger.EXPLICIT_PROVIDER_SELECTION;
+
+ return RecognitionTrigger.AUTOMATIC;
}
private @NotNull HandshakeDecision evaluatePolicy(
diff --git a/identica-engine/src/test/java/me/whereareiam/identica/engine/prepare/PreparePipelineTest.java b/identica-engine/src/test/java/me/whereareiam/identica/engine/prepare/PreparePipelineTest.java
index 16045c281..447f97ab8 100644
--- a/identica-engine/src/test/java/me/whereareiam/identica/engine/prepare/PreparePipelineTest.java
+++ b/identica-engine/src/test/java/me/whereareiam/identica/engine/prepare/PreparePipelineTest.java
@@ -31,8 +31,7 @@
import me.whereareiam.identica.handshake.policy.ProviderScopedHandshakePolicy;
import me.whereareiam.identica.identity.account.RegistrationAccountService;
import me.whereareiam.identica.identity.actor.ConnectionIdentity;
-import me.whereareiam.identica.identity.session.recognition.policy.UntrustedIpRecognitionDecision;
-import me.whereareiam.identica.identity.session.recognition.policy.UntrustedIpRecognitionPolicy;
+import me.whereareiam.identica.identity.session.recognition.eligibility.RecognitionEligibilityService;
import me.whereareiam.identica.model.auth.handshake.HandshakeDecision;
import me.whereareiam.identica.model.auth.handshake.HandshakeRequest;
import me.whereareiam.identica.model.config.Messages;
@@ -48,6 +47,7 @@
import me.whereareiam.identica.model.pipeline.state.PipelineStateReference;
import me.whereareiam.identica.model.provider.ProviderContext;
import me.whereareiam.identica.model.provider.ResolvedEntrypoint;
+import me.whereareiam.identica.model.session.recognition.eligibility.RecognitionEligibilityDecision;
import me.whereareiam.identica.pipeline.prepare.PrepareStateStore;
import me.whereareiam.identica.pipeline.state.PipelineStateStore;
import me.whereareiam.identica.provider.ProviderOperations;
@@ -71,7 +71,8 @@
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@@ -94,7 +95,7 @@ class PreparePipelineTest {
@Mock
private PipelineStateStore pipelineStateStore;
@Mock
- private UntrustedIpRecognitionPolicy untrustedIpRecognitionPolicy;
+ private RecognitionEligibilityService recognitionEligibilityService;
@Mock
private DeliveryService deliveryService;
@@ -248,8 +249,6 @@ void handshakeStageSkipsScopedPoliciesForOtherProviders() {
), 1_000L);
CountingScopedHandshakePolicy premiumPolicy = new CountingScopedHandshakePolicy("premium");
- when(untrustedIpRecognitionPolicy.evaluateAutomaticRecognition(eq("premium"), eq("127.0.0.1"), any()))
- .thenReturn(allowedDecision());
when(handshakeStore.policies()).thenReturn(java.util.Set.of(premiumPolicy));
when(pipelineStateStore.find(argThat((PipelineStateReference reference) -> connectionKey.equals(reference.getConnectionKey()))))
.thenReturn(Optional.of(pendingMigrationState));
@@ -293,7 +292,7 @@ void handshakeStageSkipsScopedPoliciesOnBlockedUntrustedIp() {
), 1_000L);
CountingScopedHandshakePolicy premiumPolicy = new CountingScopedHandshakePolicy("premium");
- when(untrustedIpRecognitionPolicy.evaluateAutomaticRecognition(eq("premium"), eq("127.0.0.1"), any()))
+ when(recognitionEligibilityService.evaluate(any()))
.thenReturn(blockedDecision());
when(handshakeStore.policies()).thenReturn(java.util.Set.of(premiumPolicy));
when(pipelineStateStore.find(argThat((PipelineStateReference reference) -> connectionKey.equals(reference.getConnectionKey()))))
@@ -338,7 +337,7 @@ void premiumAutoRecognitionHandshakePathIsSkippedOnBlockedUntrustedIp() {
), 1_000L);
CountingScopedHandshakePolicy premiumPolicy = new CountingScopedHandshakePolicy("premium");
- when(untrustedIpRecognitionPolicy.evaluateAutomaticRecognition(eq("premium"), eq("127.0.0.1"), any()))
+ when(recognitionEligibilityService.evaluate(any()))
.thenReturn(blockedDecision());
when(handshakeStore.policies()).thenReturn(java.util.Set.of(premiumPolicy));
when(pipelineStateStore.find(argThat((PipelineStateReference reference) -> connectionKey.equals(reference.getConnectionKey()))))
@@ -383,7 +382,7 @@ void handshakeStageRunsScopedPolicyWhenOverrideAllowsRecognition() {
), 1_000L);
CountingScopedHandshakePolicy premiumPolicy = new CountingScopedHandshakePolicy("premium");
- when(untrustedIpRecognitionPolicy.evaluateAutomaticRecognition(eq("premium"), eq("127.0.0.1"), any()))
+ when(recognitionEligibilityService.evaluate(any()))
.thenReturn(allowedDecision());
when(handshakeStore.policies()).thenReturn(java.util.Set.of(premiumPolicy));
when(pipelineStateStore.find(argThat((PipelineStateReference reference) -> connectionKey.equals(reference.getConnectionKey()))))
@@ -428,7 +427,7 @@ void handshakeStageRunsForExplicitProviderSelectionOnUntrustedIp() {
), 1_000L);
CountingScopedHandshakePolicy premiumPolicy = new CountingScopedHandshakePolicy("premium");
- when(untrustedIpRecognitionPolicy.evaluateAutomaticRecognition(eq("premium"), eq("127.0.0.1"), any()))
+ when(recognitionEligibilityService.evaluate(any()))
.thenReturn(allowedDecision());
when(handshakeStore.policies()).thenReturn(java.util.Set.of(premiumPolicy));
when(pipelineStateStore.find(argThat((PipelineStateReference reference) -> connectionKey.equals(reference.getConnectionKey()))))
@@ -474,7 +473,7 @@ void handshakeStageKeepsNonProviderScopedPoliciesWhenScopedRecognitionBlocked()
CountingScopedHandshakePolicy premiumPolicy = new CountingScopedHandshakePolicy("premium");
CountingHandshakePolicy globalPolicy = new CountingHandshakePolicy();
- when(untrustedIpRecognitionPolicy.evaluateAutomaticRecognition(eq("premium"), eq("127.0.0.1"), any()))
+ when(recognitionEligibilityService.evaluate(any()))
.thenReturn(blockedDecision());
when(handshakeStore.policies()).thenReturn(java.util.Set.of(premiumPolicy, globalPolicy));
when(pipelineStateStore.find(argThat((PipelineStateReference reference) -> connectionKey.equals(reference.getConnectionKey()))))
@@ -749,7 +748,7 @@ void profileStageClearsPendingMigrationWhenObservedProviderDiffers() {
new RestorePrepareStatePhase(prepareStateStore),
new ResolveEntrypointPhase(providerOperations, contextResolver),
new ResolvePendingMigrationContextPhase(pipelineStateStore),
- new EvaluateHandshakePhase(handshakeStore, untrustedIpRecognitionPolicy),
+ new EvaluateHandshakePhase(handshakeStore, recognitionEligibilityService),
new FinalizeHandshakePhase(eventManager, Messages::new),
new ResolveProfilePhase(providerOperations, contextResolver),
new ResolvePendingMigrationAccountPhase(
@@ -783,16 +782,20 @@ void profileStageClearsPendingMigrationWhenObservedProviderDiffers() {
return identity;
}
- private UntrustedIpRecognitionDecision allowedDecision() {
- return new UntrustedIpRecognitionDecision(
- UntrustedIpRecognitionDecision.Outcome.ALLOWED_IP_NOT_MATCHED
- );
+ private RecognitionEligibilityDecision allowedDecision() {
+ return RecognitionEligibilityDecision.builder()
+ .allowed(true)
+ .reason("allowed")
+ .ruleId("test-allow")
+ .build();
}
- private UntrustedIpRecognitionDecision blockedDecision() {
- return new UntrustedIpRecognitionDecision(
- UntrustedIpRecognitionDecision.Outcome.BLOCKED_UNTRUSTED_IP
- );
+ private RecognitionEligibilityDecision blockedDecision() {
+ return RecognitionEligibilityDecision.builder()
+ .allowed(false)
+ .reason("blocked")
+ .ruleId("test-block")
+ .build();
}
private static final class TestPrepareStateStore implements PrepareStateStore {