From f0b63dda739fc2b10378c9d1b5ba59d4c163a9ae Mon Sep 17 00:00:00 2001 From: imran Date: Thu, 1 Jan 2026 20:06:05 +0500 Subject: [PATCH 1/2] feat(jans-fido2): add controllers for metrics and resolve staging issues Signed-off-by: imran --- .../model/metric/Fido2MetricsAggregation.java | 34 +- .../model/metric/Fido2MetricsConstants.java | 2 +- .../fido2/model/metric/Fido2MetricsData.java | 5 +- .../fido2/model/metric/Fido2MetricsEntry.java | 55 +- .../fido2/model/metric/Fido2UserMetrics.java | 62 +-- .../fido2/service/app/AppInitializer.java | 112 +++- .../service/app/ResteasyInitializer.java | 2 + .../service/metric/Fido2AnalyticsService.java | 12 +- .../Fido2MetricsAggregationScheduler.java | 161 ++++-- .../service/metric/Fido2MetricsService.java | 176 +++++-- .../metric/Fido2UserMetricsService.java | 57 ++- .../fido2/service/shared/LoggerService.java | 1 - .../fido2/service/shared/MetricService.java | 226 +++++++- .../rs/controller/Fido2MetricsController.java | 483 ++++++++++++++++++ .../main/resources/fido2-metrics.properties | 123 +---- .../metric/Fido2MetricsTrendAnalysisTest.java | 203 -------- 16 files changed, 1218 insertions(+), 496 deletions(-) create mode 100644 jans-fido2/server/src/main/java/io/jans/fido2/ws/rs/controller/Fido2MetricsController.java delete mode 100644 jans-fido2/server/src/test/java/io/jans/fido2/service/metric/Fido2MetricsTrendAnalysisTest.java diff --git a/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2MetricsAggregation.java b/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2MetricsAggregation.java index 0369a9de8fe..81b34d49d43 100644 --- a/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2MetricsAggregation.java +++ b/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2MetricsAggregation.java @@ -16,6 +16,7 @@ import java.io.Serializable; import java.time.LocalDateTime; import java.util.Collections; +import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -40,16 +41,16 @@ public class Fido2MetricsAggregation extends Entry implements Serializable { private String aggregationType; // HOURLY, DAILY, WEEKLY, MONTHLY @AttributeName(name = "jansStartTime") - private LocalDateTime startTime; + private Date startTime; @AttributeName(name = "jansEndTime") - private LocalDateTime endTime; + private Date endTime; @AttributeName(name = "jansUniqueUsers") private Long uniqueUsers; @AttributeName(name = "jansLastUpdated") - private LocalDateTime lastUpdated; + private Date lastUpdated; /** * All metrics data stored as JSON for flexibility @@ -65,13 +66,13 @@ public Fido2MetricsAggregation() { this.metricsData = new HashMap<>(); } - public Fido2MetricsAggregation(String aggregationType, String period, LocalDateTime startTime, LocalDateTime endTime) { + public Fido2MetricsAggregation(String aggregationType, String period, Date startTime, Date endTime) { this(); this.aggregationType = aggregationType; this.id = aggregationType + "_" + period; this.startTime = startTime; this.endTime = endTime; - this.lastUpdated = LocalDateTime.now(); + this.lastUpdated = new Date(); } // Core getters and setters @@ -102,19 +103,19 @@ public String getPeriod() { return id; } - public LocalDateTime getStartTime() { + public Date getStartTime() { return startTime; } - public void setStartTime(LocalDateTime startTime) { + public void setStartTime(Date startTime) { this.startTime = startTime; } - public LocalDateTime getEndTime() { + public Date getEndTime() { return endTime; } - public void setEndTime(LocalDateTime endTime) { + public void setEndTime(Date endTime) { this.endTime = endTime; } @@ -126,11 +127,11 @@ public void setUniqueUsers(Long uniqueUsers) { this.uniqueUsers = uniqueUsers; } - public LocalDateTime getLastUpdated() { + public Date getLastUpdated() { return lastUpdated; } - public void setLastUpdated(LocalDateTime lastUpdated) { + public void setLastUpdated(Date lastUpdated) { this.lastUpdated = lastUpdated; } @@ -278,7 +279,16 @@ private Map getMapMetric(String key) { } Object value = metricsData.get(key); if (value instanceof Map) { - return (Map) value; + Map rawMap = (Map) value; + // Convert Integer values to Long for JSON serialization compatibility + Map resultMap = new HashMap<>(); + for (Map.Entry entry : rawMap.entrySet()) { + Object val = entry.getValue(); + if (val instanceof Number) { + resultMap.put(entry.getKey(), ((Number) val).longValue()); + } + } + return resultMap; } return Collections.emptyMap(); } diff --git a/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2MetricsConstants.java b/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2MetricsConstants.java index 04680c4eed6..625e1516db3 100644 --- a/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2MetricsConstants.java +++ b/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2MetricsConstants.java @@ -71,7 +71,7 @@ private Fido2MetricsConstants() { public static final String MATURE = "MATURE"; // Database Attributes - public static final String JANS_TIMESTAMP = "jansTimestamp"; + public static final String JANS_TIMESTAMP = "jansFido2MetricsTimestamp"; // Service Names public static final String METRICS_SERVICE = "metricsService"; diff --git a/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2MetricsData.java b/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2MetricsData.java index 933f842a1f7..bf3dc70d88b 100644 --- a/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2MetricsData.java +++ b/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2MetricsData.java @@ -11,6 +11,8 @@ import java.io.Serializable; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.Map; /** @@ -100,7 +102,8 @@ public class Fido2MetricsData implements Serializable { private Double cpuUsagePercent; public Fido2MetricsData() { - this.timestamp = LocalDateTime.now(); + // Use UTC timezone to align with existing FIDO2 services + this.timestamp = ZonedDateTime.now(ZoneId.of("UTC")).toLocalDateTime(); } // Getters and Setters diff --git a/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2MetricsEntry.java b/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2MetricsEntry.java index 1c013e0524a..3001c862e1c 100644 --- a/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2MetricsEntry.java +++ b/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2MetricsEntry.java @@ -7,6 +7,7 @@ package io.jans.fido2.model.metric; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import io.jans.orm.annotation.AttributeName; import io.jans.orm.annotation.DataEntry; @@ -16,6 +17,9 @@ import java.io.Serializable; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Date; import java.util.Map; import java.util.Objects; @@ -27,70 +31,71 @@ @DataEntry @ObjectClass("jansFido2MetricsEntry") @JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) public class Fido2MetricsEntry extends Entry implements Serializable { private static final long serialVersionUID = 1L; - @AttributeName(name = "jansId") + @AttributeName(name = "jansFido2MetricsId") private String id; - @AttributeName(name = "jansMetricType") + @AttributeName(name = "jansFido2MetricsType") private String metricType; - @AttributeName(name = "jansTimestamp") - private LocalDateTime timestamp; + @AttributeName(name = "jansFido2MetricsTimestamp") + private Date timestamp; - @AttributeName(name = "jansUserId") + @AttributeName(name = "jansFido2MetricsUserId") private String userId; - @AttributeName(name = "jansUsername") + @AttributeName(name = "jansFido2MetricsUsername") private String username; - @AttributeName(name = "jansOperationType") + @AttributeName(name = "jansFido2MetricsOperationType") private String operationType; // REGISTRATION, AUTHENTICATION, FALLBACK - @AttributeName(name = "jansStatus") + @AttributeName(name = "jansFido2MetricsStatus") private String status; // SUCCESS, FAILURE, ATTEMPT - @AttributeName(name = "jansDurationMs") + @AttributeName(name = "jansFido2MetricsDuration") private Long durationMs; - @AttributeName(name = "jansAuthenticatorType") + @AttributeName(name = "jansFido2MetricsAuthenticatorType") private String authenticatorType; // PLATFORM, CROSS_PLATFORM, SECURITY_KEY - @AttributeName(name = "jansDeviceInfo") + @AttributeName(name = "jansFido2MetricsDeviceInfo") @JsonObject private DeviceInfo deviceInfo; - @AttributeName(name = "jansErrorReason") + @AttributeName(name = "jansFido2MetricsErrorReason") private String errorReason; - @AttributeName(name = "jansErrorCategory") + @AttributeName(name = "jansFido2MetricsErrorCategory") private String errorCategory; - @AttributeName(name = "jansFallbackMethod") + @AttributeName(name = "jansFido2MetricsFallbackMethod") private String fallbackMethod; - @AttributeName(name = "jansFallbackReason") + @AttributeName(name = "jansFido2MetricsFallbackReason") private String fallbackReason; - @AttributeName(name = "jansUserAgent") + @AttributeName(name = "jansFido2MetricsUserAgent") private String userAgent; - @AttributeName(name = "jansIpAddress") + @AttributeName(name = "jansFido2MetricsIpAddress") private String ipAddress; - @AttributeName(name = "jansSessionId") + @AttributeName(name = "jansFido2MetricsSessionId") private String sessionId; - @AttributeName(name = "jansAdditionalData") + @AttributeName(name = "jansFido2MetricsAdditionalData") @JsonObject private transient Map additionalData; - @AttributeName(name = "jansNodeId") + @AttributeName(name = "jansFido2MetricsNodeId") private String nodeId; - @AttributeName(name = "jansApplicationType") + @AttributeName(name = "jansFido2MetricsApplicationType") private String applicationType; // Constructors @@ -102,7 +107,8 @@ public Fido2MetricsEntry(String metricType, String operationType, String status) this.metricType = metricType; this.operationType = operationType; this.status = status; - this.timestamp = LocalDateTime.now(); + // Use UTC timezone to align with FIDO2 services + this.timestamp = Date.from(ZonedDateTime.now(ZoneId.of("UTC")).toInstant()); } // Getters and Setters @@ -122,11 +128,11 @@ public void setMetricType(String metricType) { this.metricType = metricType; } - public LocalDateTime getTimestamp() { + public Date getTimestamp() { return timestamp; } - public void setTimestamp(LocalDateTime timestamp) { + public void setTimestamp(Date timestamp) { this.timestamp = timestamp; } @@ -291,6 +297,7 @@ public String toString() { * Device information extracted from User-Agent */ @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) public static class DeviceInfo implements Serializable { private static final long serialVersionUID = 1L; diff --git a/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2UserMetrics.java b/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2UserMetrics.java index e7f1bfec500..8a58999372f 100644 --- a/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2UserMetrics.java +++ b/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2UserMetrics.java @@ -15,6 +15,7 @@ import java.io.Serializable; import java.time.LocalDateTime; +import java.util.Date; import java.util.Map; import java.util.Objects; @@ -40,10 +41,10 @@ public class Fido2UserMetrics extends Entry implements Serializable { private String username; @AttributeName(name = "jansFirstRegistrationDate") - private LocalDateTime firstRegistrationDate; + private Date firstRegistrationDate; @AttributeName(name = "jansLastActivityDate") - private LocalDateTime lastActivityDate; + private Date lastActivityDate; @AttributeName(name = "jansTotalRegistrations") private Integer totalRegistrations; @@ -79,10 +80,10 @@ public class Fido2UserMetrics extends Entry implements Serializable { private String preferredOs; @AttributeName(name = "jansAvgRegistrationDuration") - private Double avgRegistrationDuration; + private Long avgRegistrationDuration; @AttributeName(name = "jansAvgAuthenticationDuration") - private Double avgAuthenticationDuration; + private Long avgAuthenticationDuration; @AttributeName(name = "jansLastIpAddress") private String lastIpAddress; @@ -102,7 +103,7 @@ public class Fido2UserMetrics extends Entry implements Serializable { private transient Map behaviorPatterns; @AttributeName(name = "jansRiskScore") - private Double riskScore; + private Long riskScore; @AttributeName(name = "jansEngagementLevel") private String engagementLevel; // HIGH, MEDIUM, LOW @@ -111,7 +112,7 @@ public class Fido2UserMetrics extends Entry implements Serializable { private String adoptionStage; // NEW, LEARNING, ADOPTED, EXPERT @AttributeName(name = "jansLastUpdated") - private LocalDateTime lastUpdated; + private Date lastUpdated; // Constructors public Fido2UserMetrics() { @@ -123,11 +124,12 @@ public Fido2UserMetrics() { this.failedAuthentications = 0; this.fallbackEvents = 0; this.isActive = true; - this.lastUpdated = LocalDateTime.now(); + this.lastUpdated = new Date(); } public Fido2UserMetrics(String userId, String username) { this(); + this.id = java.util.UUID.randomUUID().toString(); this.userId = userId; this.username = username; } @@ -157,19 +159,19 @@ public void setUsername(String username) { this.username = username; } - public LocalDateTime getFirstRegistrationDate() { + public Date getFirstRegistrationDate() { return firstRegistrationDate; } - public void setFirstRegistrationDate(LocalDateTime firstRegistrationDate) { + public void setFirstRegistrationDate(Date firstRegistrationDate) { this.firstRegistrationDate = firstRegistrationDate; } - public LocalDateTime getLastActivityDate() { + public Date getLastActivityDate() { return lastActivityDate; } - public void setLastActivityDate(LocalDateTime lastActivityDate) { + public void setLastActivityDate(Date lastActivityDate) { this.lastActivityDate = lastActivityDate; } @@ -261,19 +263,19 @@ public void setPreferredOs(String preferredOs) { this.preferredOs = preferredOs; } - public Double getAvgRegistrationDuration() { + public Long getAvgRegistrationDuration() { return avgRegistrationDuration; } - public void setAvgRegistrationDuration(Double avgRegistrationDuration) { + public void setAvgRegistrationDuration(Long avgRegistrationDuration) { this.avgRegistrationDuration = avgRegistrationDuration; } - public Double getAvgAuthenticationDuration() { + public Long getAvgAuthenticationDuration() { return avgAuthenticationDuration; } - public void setAvgAuthenticationDuration(Double avgAuthenticationDuration) { + public void setAvgAuthenticationDuration(Long avgAuthenticationDuration) { this.avgAuthenticationDuration = avgAuthenticationDuration; } @@ -317,11 +319,11 @@ public void setBehaviorPatterns(Map behaviorPatterns) { this.behaviorPatterns = behaviorPatterns; } - public Double getRiskScore() { + public Long getRiskScore() { return riskScore; } - public void setRiskScore(Double riskScore) { + public void setRiskScore(Long riskScore) { this.riskScore = riskScore; } @@ -341,11 +343,11 @@ public void setAdoptionStage(String adoptionStage) { this.adoptionStage = adoptionStage; } - public LocalDateTime getLastUpdated() { + public Date getLastUpdated() { return lastUpdated; } - public void setLastUpdated(LocalDateTime lastUpdated) { + public void setLastUpdated(Date lastUpdated) { this.lastUpdated = lastUpdated; } @@ -357,8 +359,8 @@ public void incrementRegistrations(boolean success) { } else { this.failedRegistrations++; } - this.lastActivityDate = LocalDateTime.now(); - this.lastUpdated = LocalDateTime.now(); + this.lastActivityDate = new Date(); + this.lastUpdated = new Date(); } public void incrementAuthentications(boolean success) { @@ -368,14 +370,14 @@ public void incrementAuthentications(boolean success) { } else { this.failedAuthentications++; } - this.lastActivityDate = LocalDateTime.now(); - this.lastUpdated = LocalDateTime.now(); + this.lastActivityDate = new Date(); + this.lastUpdated = new Date(); } public void incrementFallbackEvents() { this.fallbackEvents++; - this.lastActivityDate = LocalDateTime.now(); - this.lastUpdated = LocalDateTime.now(); + this.lastActivityDate = new Date(); + this.lastUpdated = new Date(); } public double getRegistrationSuccessRate() { @@ -415,13 +417,15 @@ public double getFallbackRate() { } public boolean isNewUser() { - return firstRegistrationDate != null && - firstRegistrationDate.isAfter(LocalDateTime.now().minusDays(30)); + if (firstRegistrationDate == null) return false; + long thirtyDaysAgo = System.currentTimeMillis() - (30L * 24 * 60 * 60 * 1000); + return firstRegistrationDate.getTime() > thirtyDaysAgo; } public boolean isActiveUser() { - return lastActivityDate != null && - lastActivityDate.isAfter(LocalDateTime.now().minusDays(30)); + if (lastActivityDate == null) return false; + long thirtyDaysAgo = System.currentTimeMillis() - (30L * 24 * 60 * 60 * 1000); + return lastActivityDate.getTime() > thirtyDaysAgo; } public void updateEngagementLevel() { diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/app/AppInitializer.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/app/AppInitializer.java index 80cc51bf0bc..73ea4a21739 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/app/AppInitializer.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/app/AppInitializer.java @@ -105,7 +105,7 @@ public class AppInitializer { private MDS3UpdateTimer mds3UpdateTimer; @Inject - private io.jans.fido2.service.metric.Fido2MetricsAggregationScheduler fido2MetricsAggregationScheduler; + private Instance fido2MetricsAggregationSchedulerInstance; @PostConstruct public void createApplicationComponents() { @@ -117,36 +117,114 @@ public void createApplicationComponents() { } public void applicationInitialized(@Observes @Initialized(ApplicationScoped.class) Object init) { + log.info("=== FIDO2 Application Initialization Started ==="); log.debug("Initializing application services"); - configurationFactory.create(); + try { + configurationFactory.create(); + log.info("Configuration factory created successfully"); + } catch (Exception e) { + log.error("Failed to create configuration factory: {}", e.getMessage(), e); + return; + } - PersistenceEntryManager localPersistenceEntryManager = persistenceEntryManagerInstance.get(); - log.trace("Attempting to use {}: {}", ApplicationFactory.PERSISTENCE_ENTRY_MANAGER_NAME, - localPersistenceEntryManager.getOperationService()); + try { + PersistenceEntryManager localPersistenceEntryManager = persistenceEntryManagerInstance.get(); + log.trace("Attempting to use {}: {}", ApplicationFactory.PERSISTENCE_ENTRY_MANAGER_NAME, + localPersistenceEntryManager.getOperationService()); + log.info("Persistence entry manager initialized successfully"); + } catch (Exception e) { + log.error("Failed to initialize persistence entry manager: {}", e.getMessage(), e); + return; + } // Initialize python interpreter - pythonService - .initPythonInterpreter(configurationFactory.getBaseConfiguration().getString("pythonModulesDir", null)); + try { + pythonService + .initPythonInterpreter(configurationFactory.getBaseConfiguration().getString("pythonModulesDir", null)); + log.info("Python interpreter initialized successfully"); + } catch (Exception e) { + log.error("Failed to initialize Python interpreter: {}", e.getMessage(), e); + // Continue anyway - Python might not be critical + } // Initialize script manager List supportedCustomScriptTypes = Lists.newArrayList(CustomScriptType.FIDO2_EXTENSION); // Start timer - initSchedulerService(); + try { + initSchedulerService(); + log.info("Scheduler service initialized successfully"); + } catch (Exception e) { + log.error("Failed to initialize scheduler service: {}", e.getMessage(), e); + return; + } // Schedule timer tasks - metricService.initTimer(); - configurationFactory.initTimer(); - loggerService.initTimer(true); - cleanerTimer.initTimer(); - mds3UpdateTimer.initTimer(); - customScriptManager.initTimer(supportedCustomScriptTypes); - fido2MetricsAggregationScheduler.initTimer(); + try { + metricService.initTimer(); + log.info("Metric service timer initialized"); + } catch (Exception e) { + log.error("Failed to initialize metric service timer: {}", e.getMessage(), e); + } + + try { + configurationFactory.initTimer(); + log.info("Configuration factory timer initialized"); + } catch (Exception e) { + log.error("Failed to initialize configuration factory timer: {}", e.getMessage(), e); + } + + try { + loggerService.initTimer(true); + log.info("Logger service timer initialized"); + } catch (Exception e) { + log.error("Failed to initialize logger service timer: {}", e.getMessage(), e); + } + + try { + cleanerTimer.initTimer(); + log.info("Cleaner timer initialized"); + } catch (Exception e) { + log.error("Failed to initialize cleaner timer: {}", e.getMessage(), e); + } + + try { + mds3UpdateTimer.initTimer(); + log.info("MDS3 update timer initialized"); + } catch (Exception e) { + log.error("Failed to initialize MDS3 update timer: {}", e.getMessage(), e); + } + + try { + customScriptManager.initTimer(supportedCustomScriptTypes); + log.info("Custom script manager timer initialized"); + } catch (Exception e) { + log.error("Failed to initialize custom script manager timer: {}", e.getMessage(), e); + } + + // Initialize FIDO2 metrics aggregation scheduler (optional - might not be available) + try { + if (fido2MetricsAggregationSchedulerInstance != null && !fido2MetricsAggregationSchedulerInstance.isUnsatisfied()) { + fido2MetricsAggregationSchedulerInstance.get().initTimer(); + log.info("FIDO2 metrics aggregation scheduler initialized"); + } else { + log.info("FIDO2 metrics aggregation scheduler not available, skipping initialization"); + } + } catch (Exception e) { + log.warn("Failed to initialize FIDO2 metrics aggregation scheduler: {}", e.getMessage(), e); + } // Notify plugins about finish application initialization - eventApplicationInitialized.select(ApplicationInitialized.Literal.APPLICATION) - .fire(new ApplicationInitializedEvent()); + try { + eventApplicationInitialized.select(ApplicationInitialized.Literal.APPLICATION) + .fire(new ApplicationInitializedEvent()); + log.info("Application initialization event fired successfully"); + } catch (Exception e) { + log.error("Failed to fire application initialized event: {}", e.getMessage(), e); + } + + log.info("=== FIDO2 Application Initialization Completed Successfully ==="); } protected void initSchedulerService() { diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/app/ResteasyInitializer.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/app/ResteasyInitializer.java index afda80660ac..d1063463c45 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/app/ResteasyInitializer.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/app/ResteasyInitializer.java @@ -16,6 +16,7 @@ import io.jans.fido2.ws.rs.controller.AssertionController; import io.jans.fido2.ws.rs.controller.AttestationController; import io.jans.fido2.ws.rs.controller.ConfigurationController; +import io.jans.fido2.ws.rs.controller.Fido2MetricsController; /** * Integration with Resteasy @@ -33,6 +34,7 @@ public Set> getClasses() { classes.add(AssertionController.class); classes.add(AttestationController.class); classes.add(WebAuthnController.class); + classes.add(Fido2MetricsController.class); return classes; } diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/metric/Fido2AnalyticsService.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/metric/Fido2AnalyticsService.java index a15fe714dc3..ef22bea4463 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/metric/Fido2AnalyticsService.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/metric/Fido2AnalyticsService.java @@ -16,6 +16,8 @@ import org.slf4j.Logger; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.*; import java.util.ResourceBundle; import java.util.stream.Collectors; @@ -87,8 +89,9 @@ public Map generateComprehensiveReport(LocalDateTime startTime, report.put("recommendations", generateRecommendations(startTime, endTime)); // Report metadata + // Use UTC timezone to align with FIDO2 services report.put("reportMetadata", Map.of( - "generatedAt", LocalDateTime.now(), + "generatedAt", ZonedDateTime.now(ZoneId.of("UTC")).toLocalDateTime(), "startTime", startTime, "endTime", endTime, "reportType", "COMPREHENSIVE" @@ -561,7 +564,12 @@ private Map analyzeSeasonalPatterns(List dayOfWeekUsage = new HashMap<>(); for (Fido2MetricsAggregation agg : aggregations) { - String dayOfWeek = agg.getStartTime().getDayOfWeek().name(); + // Convert Date to LocalDateTime to get day of week + LocalDateTime dateTime = LocalDateTime.ofInstant( + agg.getStartTime().toInstant(), + ZoneId.of("UTC") + ); + String dayOfWeek = dateTime.getDayOfWeek().name(); long totalAttempts = getTotalAttempts(agg); dayOfWeekUsage.merge(dayOfWeek, totalAttempts, Long::sum); } diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/metric/Fido2MetricsAggregationScheduler.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/metric/Fido2MetricsAggregationScheduler.java index 25f10226dea..871ef9ff9fa 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/metric/Fido2MetricsAggregationScheduler.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/metric/Fido2MetricsAggregationScheduler.java @@ -10,6 +10,7 @@ import io.jans.fido2.model.metric.Fido2MetricsConstants; import io.jans.fido2.service.cluster.Fido2ClusterNodeService; import io.jans.model.cluster.ClusterNode; +import jakarta.enterprise.inject.spi.CDI; import io.jans.service.timer.QuartzSchedulerManager; import jakarta.annotation.PreDestroy; import jakarta.enterprise.context.ApplicationScoped; @@ -29,6 +30,8 @@ import java.util.ResourceBundle; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.List; import java.util.concurrent.Executors; @@ -54,16 +57,31 @@ public class Fido2MetricsAggregationScheduler { @Inject private Fido2MetricsService metricsService; - @Inject + // Cluster node service - optional, only available in multi-node deployments private Fido2ClusterNodeService clusterNodeService; + + // Flag to track if we're in a cluster environment + private boolean isClusterEnvironment = false; @Inject private QuartzSchedulerManager quartzSchedulerManager; - // Scheduled executor for periodic lock updates during aggregation - private final ScheduledExecutorService updateExecutor = Executors.newScheduledThreadPool(1); + // Scheduled executor for periodic lock updates during aggregation (only used in cluster mode) + private ScheduledExecutorService updateExecutor; - private static final ResourceBundle METRICS_CONFIG = ResourceBundle.getBundle("fido2-metrics"); + // Load properties file safely - don't fail if missing + private static final ResourceBundle METRICS_CONFIG; + + static { + ResourceBundle bundle = null; + try { + bundle = ResourceBundle.getBundle("fido2-metrics"); + } catch (Exception e) { + // Properties file not found - will use hardcoded defaults + System.err.println("WARN: fido2-metrics.properties not found, using defaults"); + } + METRICS_CONFIG = bundle; + } /** * Helper method to execute aggregation job with distributed locking @@ -83,15 +101,27 @@ private static void executeAggregationJob(JobExecutionContext context, String jo return; } - ScheduledFuture updateTask = scheduler.startPeriodicLockUpdates(); - if (updateTask == null) { - log.warn("Skipping {} aggregation - failed to start periodic lock updates or lock not held", jobType); - return; + // In cluster mode, try to start periodic lock updates + // If cluster mode fails, fall back to single-node mode and still run aggregation + ScheduledFuture updateTask = null; + boolean isClusterMode = scheduler.isClusterEnvironment; + if (isClusterMode) { + updateTask = scheduler.startPeriodicLockUpdates(); + if (updateTask == null) { + // Cluster mode failed (e.g., cluster config missing), but shouldPerformAggregation + // already returned true (fallback to single-node), so we can still run aggregation + log.warn("Cluster lock updates failed for {} aggregation, running in single-node mode: {}", + jobType, "ou=node is not configured in static configuration"); + // Continue to run aggregation in single-node mode + } } + try { aggregationTask.accept(metricsService); } finally { - updateTask.cancel(false); + if (updateTask != null) { + updateTask.cancel(false); + } } } } catch (Exception e) { @@ -112,8 +142,11 @@ public static class HourlyAggregationJob implements Job { @Override public void execute(JobExecutionContext context) throws JobExecutionException { executeAggregationJob(context, "hourly", metricsService -> { - LocalDateTime previousHour = LocalDateTime.now().minusHours(1) - .truncatedTo(ChronoUnit.HOURS); + // Use UTC timezone to align with FIDO2 services + LocalDateTime previousHour = ZonedDateTime.now(ZoneId.of("UTC")) + .minusHours(1) + .truncatedTo(ChronoUnit.HOURS) + .toLocalDateTime(); metricsService.createHourlyAggregation(previousHour); log.info("Hourly aggregation completed for: {}", previousHour); }); @@ -129,8 +162,11 @@ public static class DailyAggregationJob implements Job { @Override public void execute(JobExecutionContext context) throws JobExecutionException { executeAggregationJob(context, "daily", metricsService -> { - LocalDateTime previousDay = LocalDateTime.now().minusDays(1) - .truncatedTo(ChronoUnit.DAYS); + // Use UTC timezone to align with FIDO2 services + LocalDateTime previousDay = ZonedDateTime.now(ZoneId.of("UTC")) + .minusDays(1) + .truncatedTo(ChronoUnit.DAYS) + .toLocalDateTime(); metricsService.createDailyAggregation(previousDay); log.info("Daily aggregation completed for: {}", previousDay); }); @@ -146,9 +182,12 @@ public static class WeeklyAggregationJob implements Job { @Override public void execute(JobExecutionContext context) throws JobExecutionException { executeAggregationJob(context, "weekly", metricsService -> { - LocalDateTime previousWeek = LocalDateTime.now().minusWeeks(1) + // Use UTC timezone to align with FIDO2 services + LocalDateTime previousWeek = ZonedDateTime.now(ZoneId.of("UTC")) + .minusWeeks(1) .with(java.time.DayOfWeek.MONDAY) - .truncatedTo(ChronoUnit.DAYS); + .truncatedTo(ChronoUnit.DAYS) + .toLocalDateTime(); metricsService.createWeeklyAggregation(previousWeek); log.info("Weekly aggregation completed for: {}", previousWeek); }); @@ -164,9 +203,12 @@ public static class MonthlyAggregationJob implements Job { @Override public void execute(JobExecutionContext context) throws JobExecutionException { executeAggregationJob(context, "monthly", metricsService -> { - LocalDateTime previousMonth = LocalDateTime.now().minusMonths(1) + // Use UTC timezone to align with FIDO2 services + LocalDateTime previousMonth = ZonedDateTime.now(ZoneId.of("UTC")) + .minusMonths(1) .withDayOfMonth(1) - .truncatedTo(ChronoUnit.DAYS); + .truncatedTo(ChronoUnit.DAYS) + .toLocalDateTime(); metricsService.createMonthlyAggregation(previousMonth); log.info("Monthly aggregation completed for: {}", previousMonth); }); @@ -240,7 +282,19 @@ public void flushPendingMetrics() { * Uses distributed locking via ClusterNode with getClusterNodesLive() pattern * Synchronized to prevent race conditions between concurrent job threads */ + /** + * Check if this node should perform the aggregation + * In single-node deployments: always returns true + * In multi-node clusters: uses distributed locking to coordinate + * Synchronized to prevent race conditions between concurrent job threads + */ public synchronized boolean shouldPerformAggregation() { + // Single-node deployment: always perform aggregation + if (!isClusterEnvironment) { + return true; + } + + // Multi-node cluster: use distributed locking try { List liveList = clusterNodeService.getClusterNodesLive(); @@ -250,11 +304,15 @@ public synchronized boolean shouldPerformAggregation() { return checkIfWeHoldLock(liveList); } } catch (Exception e) { - log.error("Error checking aggregation lock", e); - return false; + log.warn("Error checking aggregation lock in cluster environment, falling back to single-node behavior: {}", + e.getMessage()); + // Fallback to single-node behavior if cluster coordination fails + return true; } } + // ========== CLUSTER SUPPORT (OPTIONAL - AUTO-DETECTS ENVIRONMENT) ========== + /** * Try to acquire the lock when no live nodes exist */ @@ -304,11 +362,16 @@ private boolean checkIfWeHoldLock(List liveList) { /** * Start periodic updates to keep the lock alive during aggregation work * Should be called at the start of aggregation work + * Only used in cluster environments * - * @return ScheduledFuture that can be used to cancel the updates + * @return ScheduledFuture that can be used to cancel the updates, or null if not in cluster */ @SuppressWarnings("java:S1452") // Wildcard type is required as ScheduledExecutorService.scheduleAtFixedRate returns ScheduledFuture public ScheduledFuture startPeriodicLockUpdates() { + if (!isClusterEnvironment || updateExecutor == null) { + return null; // Not in cluster or executor not initialized + } + try { // Get the current live node that we hold List liveList = clusterNodeService.getClusterNodesLive(); @@ -348,21 +411,29 @@ public ScheduledFuture startPeriodicLockUpdates() { /** * Initialize cluster node service - * Called during application startup - * With the new pattern, we don't need to pre-allocate a node - * The lock is acquired on-demand when aggregation jobs run + * Detects if we're in a cluster environment and initializes accordingly + * NOTE: Removed @PostConstruct to avoid blocking during bean creation */ - public void initializeClusterNode() { - if (!isAggregationEnabled()) { - log.info("FIDO2 metrics aggregation is disabled"); - return; + private void initializeClusterEnvironment() { + // Try to detect cluster environment by checking if cluster service exists + try { + // Attempt to get cluster service via CDI lookup + clusterNodeService = CDI.current().select(Fido2ClusterNodeService.class).get(); + isClusterEnvironment = true; + // Initialize update executor for cluster coordination + updateExecutor = Executors.newScheduledThreadPool(1); + log.info("FIDO2 metrics aggregation enabled in CLUSTER mode - distributed locking will be used"); + } catch (Exception e) { + // Cluster service not available - single node deployment + isClusterEnvironment = false; + updateExecutor = null; + log.info("FIDO2 metrics aggregation enabled in SINGLE-NODE mode - no cluster coordination needed"); } - log.info("FIDO2 metrics aggregation enabled - locks will be acquired on-demand"); } /** * Initialize and register Quartz jobs for metrics aggregation - * Called during application startup + * Called during application startup by AppInitializer */ public void initTimer() { if (!isAggregationEnabled()) { @@ -370,6 +441,9 @@ public void initTimer() { return; } + // Initialize cluster environment detection (moved from @PostConstruct to avoid blocking) + initializeClusterEnvironment(); + try { // Prepare JobDataMap with required services JobDataMap jobDataMap = new JobDataMap(); @@ -378,28 +452,28 @@ public void initTimer() { // Register hourly aggregation job if (isAggregationTypeEnabled(Fido2MetricsConstants.HOURLY)) { - String hourlyCron = getConfigString("fido2.metrics.aggregation.hourly.cron", "0 5 * * * *"); + String hourlyCron = getConfigString("fido2.metrics.aggregation.hourly.cron", "0 5 * * * ?"); registerAggregationJob("HourlyAggregation", HourlyAggregationJob.class, hourlyCron, jobDataMap); log.info("Registered hourly FIDO2 metrics aggregation job with cron: {}", hourlyCron); } // Register daily aggregation job if (isAggregationTypeEnabled(Fido2MetricsConstants.DAILY)) { - String dailyCron = getConfigString("fido2.metrics.aggregation.daily.cron", "0 10 1 * * *"); + String dailyCron = getConfigString("fido2.metrics.aggregation.daily.cron", "0 10 1 * * ?"); registerAggregationJob("DailyAggregation", DailyAggregationJob.class, dailyCron, jobDataMap); log.info("Registered daily FIDO2 metrics aggregation job with cron: {}", dailyCron); } // Register weekly aggregation job if (isAggregationTypeEnabled(Fido2MetricsConstants.WEEKLY)) { - String weeklyCron = getConfigString("fido2.metrics.aggregation.weekly.cron", "0 15 1 * * MON"); + String weeklyCron = getConfigString("fido2.metrics.aggregation.weekly.cron", "0 15 1 ? * MON"); registerAggregationJob("WeeklyAggregation", WeeklyAggregationJob.class, weeklyCron, jobDataMap); log.info("Registered weekly FIDO2 metrics aggregation job with cron: {}", weeklyCron); } // Register monthly aggregation job if (isAggregationTypeEnabled(Fido2MetricsConstants.MONTHLY)) { - String monthlyCron = getConfigString("fido2.metrics.aggregation.monthly.cron", "0 20 1 1 * *"); + String monthlyCron = getConfigString("fido2.metrics.aggregation.monthly.cron", "0 20 1 1 * ?"); registerAggregationJob("MonthlyAggregation", MonthlyAggregationJob.class, monthlyCron, jobDataMap); log.info("Registered monthly FIDO2 metrics aggregation job with cron: {}", monthlyCron); } @@ -465,21 +539,24 @@ private String getConfigString(String key, String defaultValue) { /** * Cleanup resources on shutdown - * Shutdown the executor service used for periodic lock updates + * Shuts down update executor if in cluster mode */ @PreDestroy public void releaseClusterNode() { - try { - updateExecutor.shutdown(); - if (!updateExecutor.awaitTermination(5, TimeUnit.SECONDS)) { + if (updateExecutor != null) { + try { + updateExecutor.shutdown(); + if (!updateExecutor.awaitTermination(5, TimeUnit.SECONDS)) { + updateExecutor.shutdownNow(); + } + log.info("Shutdown periodic lock update executor for FIDO2 metrics aggregation"); + } catch (InterruptedException e) { updateExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + log.warn("Interrupted while shutting down lock update executor", e); } - log.info("Shutdown periodic lock update executor for FIDO2 metrics aggregation"); - } catch (InterruptedException e) { - updateExecutor.shutdownNow(); - Thread.currentThread().interrupt(); - log.warn("Interrupted while shutting down lock update executor", e); } + log.info("FIDO2 metrics aggregation scheduler shutdown"); } } diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/metric/Fido2MetricsService.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/metric/Fido2MetricsService.java index 8b3a1b7f2d6..bd8e70820c3 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/metric/Fido2MetricsService.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/metric/Fido2MetricsService.java @@ -20,6 +20,8 @@ import org.slf4j.Logger; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.CompletableFuture; @@ -86,9 +88,13 @@ public void storeMetricsData(Fido2MetricsData metricsData) { */ public List getMetricsEntries(LocalDateTime startTime, LocalDateTime endTime) { try { + // Convert LocalDateTime to Date for SQL persistence filters + Date startDate = Date.from(startTime.atZone(ZoneId.of("UTC")).toInstant()); + Date endDate = Date.from(endTime.atZone(ZoneId.of("UTC")).toInstant()); + Filter filter = Filter.createANDFilter( - Filter.createGreaterOrEqualFilter(Fido2MetricsConstants.JANS_TIMESTAMP, startTime), - Filter.createLessOrEqualFilter(Fido2MetricsConstants.JANS_TIMESTAMP, endTime) + Filter.createGreaterOrEqualFilter(Fido2MetricsConstants.JANS_TIMESTAMP, startDate), + Filter.createLessOrEqualFilter(Fido2MetricsConstants.JANS_TIMESTAMP, endDate) ); List entries = persistenceEntryManager.findEntries( @@ -107,10 +113,14 @@ public List getMetricsEntries(LocalDateTime startTime, LocalD */ public List getMetricsEntriesByUser(String userId, LocalDateTime startTime, LocalDateTime endTime) { try { + // Convert LocalDateTime to Date for SQL persistence filters + Date startDate = Date.from(startTime.atZone(ZoneId.of("UTC")).toInstant()); + Date endDate = Date.from(endTime.atZone(ZoneId.of("UTC")).toInstant()); + Filter filter = Filter.createANDFilter( - Filter.createEqualityFilter("jansUserId", userId), - Filter.createGreaterOrEqualFilter(Fido2MetricsConstants.JANS_TIMESTAMP, startTime), - Filter.createLessOrEqualFilter(Fido2MetricsConstants.JANS_TIMESTAMP, endTime) + Filter.createEqualityFilter("jansFido2MetricsUserId", userId), + Filter.createGreaterOrEqualFilter(Fido2MetricsConstants.JANS_TIMESTAMP, startDate), + Filter.createLessOrEqualFilter(Fido2MetricsConstants.JANS_TIMESTAMP, endDate) ); return persistenceEntryManager.findEntries( @@ -127,10 +137,14 @@ public List getMetricsEntriesByUser(String userId, LocalDateT */ public List getMetricsEntriesByOperation(String operationType, LocalDateTime startTime, LocalDateTime endTime) { try { + // Convert LocalDateTime to Date for SQL persistence filters + Date startDate = Date.from(startTime.atZone(ZoneId.of("UTC")).toInstant()); + Date endDate = Date.from(endTime.atZone(ZoneId.of("UTC")).toInstant()); + Filter filter = Filter.createANDFilter( - Filter.createEqualityFilter("jansOperationType", operationType), - Filter.createGreaterOrEqualFilter(Fido2MetricsConstants.JANS_TIMESTAMP, startTime), - Filter.createLessOrEqualFilter(Fido2MetricsConstants.JANS_TIMESTAMP, endTime) + Filter.createEqualityFilter("jansFido2MetricsOperationType", operationType), + Filter.createGreaterOrEqualFilter(Fido2MetricsConstants.JANS_TIMESTAMP, startDate), + Filter.createLessOrEqualFilter(Fido2MetricsConstants.JANS_TIMESTAMP, endDate) ); return persistenceEntryManager.findEntries( @@ -270,13 +284,21 @@ public void createMonthlyAggregation(LocalDateTime month) { /** * Get aggregations by time range + * Uses interval overlap logic: finds aggregations that overlap with the query range + * An aggregation overlaps if: aggregation.startTime <= queryEndTime AND aggregation.endTime >= queryStartTime */ public List getAggregations(String aggregationType, LocalDateTime startTime, LocalDateTime endTime) { try { + // Convert LocalDateTime to Date for SQL persistence filters + Date startDate = Date.from(startTime.atZone(ZoneId.of("UTC")).toInstant()); + Date endDate = Date.from(endTime.atZone(ZoneId.of("UTC")).toInstant()); + + // Interval overlap check: aggregation overlaps query if: + // aggregation.startTime <= queryEndTime AND aggregation.endTime >= queryStartTime Filter filter = Filter.createANDFilter( Filter.createEqualityFilter("jansAggregationType", aggregationType), - Filter.createGreaterOrEqualFilter("jansStartTime", startTime), - Filter.createLessOrEqualFilter("jansEndTime", endTime) + Filter.createLessOrEqualFilter("jansStartTime", endDate), // aggregation starts before/at query end + Filter.createGreaterOrEqualFilter("jansEndTime", startDate) // aggregation ends after/at query start ); return persistenceEntryManager.findEntries( @@ -298,7 +320,13 @@ public void cleanupOldData(int retentionDays) { CompletableFuture.runAsync(() -> { try { - LocalDateTime cutoffDate = LocalDateTime.now().minusDays(retentionDays); + // Use UTC timezone to align with FIDO2 services + LocalDateTime cutoffDateTime = ZonedDateTime.now(ZoneId.of("UTC")) + .toLocalDateTime() + .minusDays(retentionDays); + + // Convert LocalDateTime to Date for SQL persistence filters + Date cutoffDate = Date.from(cutoffDateTime.atZone(ZoneId.of("UTC")).toInstant()); // Cleanup old metrics entries Filter filter = Filter.createLessOrEqualFilter(Fido2MetricsConstants.JANS_TIMESTAMP, cutoffDate); @@ -489,7 +517,11 @@ private Fido2MetricsAggregation calculateAggregation(String aggregationType, Str return null; } - Fido2MetricsAggregation aggregation = new Fido2MetricsAggregation(aggregationType, period, startTime, endTime); + // Convert LocalDateTime to Date for ORM persistence + Date startDate = Date.from(startTime.atZone(ZoneId.of("UTC")).toInstant()); + Date endDate = Date.from(endTime.atZone(ZoneId.of("UTC")).toInstant()); + + Fido2MetricsAggregation aggregation = new Fido2MetricsAggregation(aggregationType, period, startDate, endDate); Map metricsData = new HashMap<>(); // Calculate registration metrics @@ -595,9 +627,13 @@ private Fido2MetricsAggregation calculateAggregation(String aggregationType, Str private List getMetricsEntriesByTimeRange(LocalDateTime startTime, LocalDateTime endTime) { try { + // Convert LocalDateTime to Date for SQL persistence filters + Date startDate = Date.from(startTime.atZone(ZoneId.of("UTC")).toInstant()); + Date endDate = Date.from(endTime.atZone(ZoneId.of("UTC")).toInstant()); + Filter filter = Filter.createANDFilter( - Filter.createGreaterOrEqualFilter(Fido2MetricsConstants.JANS_TIMESTAMP, startTime), - Filter.createLessOrEqualFilter(Fido2MetricsConstants.JANS_TIMESTAMP, endTime) + Filter.createGreaterOrEqualFilter(Fido2MetricsConstants.JANS_TIMESTAMP, startDate), + Filter.createLessOrEqualFilter(Fido2MetricsConstants.JANS_TIMESTAMP, endDate) ); return persistenceEntryManager.findEntries( @@ -626,34 +662,96 @@ private String generateAggregationDn(String id) { private Fido2MetricsEntry convertToMetricsEntry(Fido2MetricsData metricsData) { Fido2MetricsEntry entry = new Fido2MetricsEntry(); entry.setId(UUID.randomUUID().toString()); - entry.setMetricType(metricsData.getMetricType()); - entry.setTimestamp(metricsData.getTimestamp()); + + // Convert LocalDateTime to Date for ORM compatibility (already in UTC) + if (metricsData.getTimestamp() != null) { + entry.setTimestamp(Date.from(metricsData.getTimestamp().atZone(ZoneId.of("UTC")).toInstant())); + } + + // Essential fields - always set entry.setUserId(metricsData.getUserId()); entry.setUsername(metricsData.getUsername()); entry.setOperationType(metricsData.getOperationType()); entry.setStatus(metricsData.getOperationStatus()); - entry.setDurationMs(metricsData.getDurationMs()); - entry.setAuthenticatorType(metricsData.getAuthenticatorType()); - entry.setErrorReason(metricsData.getErrorReason()); - entry.setErrorCategory(metricsData.getErrorCategory()); - entry.setFallbackMethod(metricsData.getFallbackMethod()); - entry.setFallbackReason(metricsData.getFallbackReason()); - entry.setSessionId(metricsData.getSessionId()); - entry.setIpAddress(metricsData.getIpAddress()); - entry.setUserAgent(metricsData.getUserAgent()); - entry.setNodeId(metricsData.getNodeId()); - entry.setApplicationType(metricsData.getApplicationType()); - - // Convert device info + + // Performance metrics - only set if available + if (metricsData.getDurationMs() != null) { + entry.setDurationMs(metricsData.getDurationMs()); + } + + // Authenticator info - only set if available + if (metricsData.getAuthenticatorType() != null && !metricsData.getAuthenticatorType().trim().isEmpty()) { + entry.setAuthenticatorType(metricsData.getAuthenticatorType()); + } + + // Error info - only set for failures + if (metricsData.getErrorReason() != null && !metricsData.getErrorReason().trim().isEmpty()) { + entry.setErrorReason(metricsData.getErrorReason()); + } + if (metricsData.getErrorCategory() != null && !metricsData.getErrorCategory().trim().isEmpty()) { + entry.setErrorCategory(metricsData.getErrorCategory()); + } + + // Fallback info - only set for fallback events + if (metricsData.getFallbackMethod() != null && !metricsData.getFallbackMethod().trim().isEmpty()) { + entry.setFallbackMethod(metricsData.getFallbackMethod()); + } + if (metricsData.getFallbackReason() != null && !metricsData.getFallbackReason().trim().isEmpty()) { + entry.setFallbackReason(metricsData.getFallbackReason()); + } + + // Network info - only set if available + if (metricsData.getIpAddress() != null && !metricsData.getIpAddress().trim().isEmpty()) { + entry.setIpAddress(metricsData.getIpAddress()); + } + if (metricsData.getUserAgent() != null && !metricsData.getUserAgent().trim().isEmpty()) { + entry.setUserAgent(metricsData.getUserAgent()); + } + + // Session info - only set if available + if (metricsData.getSessionId() != null && !metricsData.getSessionId().trim().isEmpty()) { + entry.setSessionId(metricsData.getSessionId()); + } + + // Cluster info - only set if available (useful for multi-node deployments) + if (metricsData.getNodeId() != null && !metricsData.getNodeId().trim().isEmpty()) { + entry.setNodeId(metricsData.getNodeId()); + } + + // Convert device info - only set if available and non-empty if (metricsData.getDeviceInfo() != null) { Fido2MetricsEntry.DeviceInfo deviceInfo = new Fido2MetricsEntry.DeviceInfo(); - deviceInfo.setBrowser(metricsData.getDeviceInfo().getBrowser()); - deviceInfo.setBrowserVersion(metricsData.getDeviceInfo().getBrowserVersion()); - deviceInfo.setOs(metricsData.getDeviceInfo().getOperatingSystem()); - deviceInfo.setOsVersion(metricsData.getDeviceInfo().getOsVersion()); - deviceInfo.setDeviceType(metricsData.getDeviceInfo().getDeviceType()); - deviceInfo.setUserAgent(metricsData.getDeviceInfo().getUserAgent()); - entry.setDeviceInfo(deviceInfo); + boolean hasDeviceInfo = false; + + if (metricsData.getDeviceInfo().getBrowser() != null && !metricsData.getDeviceInfo().getBrowser().trim().isEmpty()) { + deviceInfo.setBrowser(metricsData.getDeviceInfo().getBrowser()); + hasDeviceInfo = true; + } + if (metricsData.getDeviceInfo().getBrowserVersion() != null && !metricsData.getDeviceInfo().getBrowserVersion().trim().isEmpty()) { + deviceInfo.setBrowserVersion(metricsData.getDeviceInfo().getBrowserVersion()); + hasDeviceInfo = true; + } + if (metricsData.getDeviceInfo().getOperatingSystem() != null && !metricsData.getDeviceInfo().getOperatingSystem().trim().isEmpty()) { + deviceInfo.setOs(metricsData.getDeviceInfo().getOperatingSystem()); + hasDeviceInfo = true; + } + if (metricsData.getDeviceInfo().getOsVersion() != null && !metricsData.getDeviceInfo().getOsVersion().trim().isEmpty()) { + deviceInfo.setOsVersion(metricsData.getDeviceInfo().getOsVersion()); + hasDeviceInfo = true; + } + if (metricsData.getDeviceInfo().getDeviceType() != null && !metricsData.getDeviceInfo().getDeviceType().trim().isEmpty()) { + deviceInfo.setDeviceType(metricsData.getDeviceInfo().getDeviceType()); + hasDeviceInfo = true; + } + if (metricsData.getDeviceInfo().getUserAgent() != null && !metricsData.getDeviceInfo().getUserAgent().trim().isEmpty()) { + deviceInfo.setUserAgent(metricsData.getDeviceInfo().getUserAgent()); + hasDeviceInfo = true; + } + + // Only set deviceInfo if we have at least one field populated + if (hasDeviceInfo) { + entry.setDeviceInfo(deviceInfo); + } } return entry; @@ -712,7 +810,11 @@ public Map getPeriodOverPeriodComparison(String aggregationType, try { java.time.temporal.ChronoUnit chronoUnit = getChronoUnitForAggregationType(aggregationType); - LocalDateTime endTime = alignEndToBoundary(LocalDateTime.now(), chronoUnit); + // Use UTC timezone to align with FIDO2 services + LocalDateTime endTime = alignEndToBoundary( + ZonedDateTime.now(ZoneId.of("UTC")).toLocalDateTime(), + chronoUnit + ); LocalDateTime startTime = endTime.minus(periods, chronoUnit); List currentPeriod = getAggregations(aggregationType, startTime, endTime); diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/metric/Fido2UserMetricsService.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/metric/Fido2UserMetricsService.java index fa3ecdd0dd5..ee7b9afe0b0 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/metric/Fido2UserMetricsService.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/metric/Fido2UserMetricsService.java @@ -18,7 +18,10 @@ import org.slf4j.Logger; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.*; +import java.util.Date; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; @@ -65,10 +68,12 @@ public void updateUserRegistrationMetrics(UserMetricsUpdateRequest request) { CompletableFuture.runAsync(() -> { try { - Fido2UserMetrics userMetrics = getUserMetrics(request.getUserId()); + // Use username for lookup since userId is often null in FIDO2 flows + Fido2UserMetrics userMetrics = getUserMetricsByUsername(request.getUsername()); if (userMetrics == null) { userMetrics = new Fido2UserMetrics(request.getUserId(), request.getUsername()); - userMetrics.setFirstRegistrationDate(LocalDateTime.now()); + // Use UTC timezone to align with FIDO2 services + userMetrics.setFirstRegistrationDate(Date.from(ZonedDateTime.now(ZoneId.of("UTC")).toInstant())); } userMetrics.incrementRegistrations(request.isSuccess()); @@ -101,7 +106,8 @@ public void updateUserAuthenticationMetrics(UserMetricsUpdateRequest request) { CompletableFuture.runAsync(() -> { try { - Fido2UserMetrics userMetrics = getUserMetrics(request.getUserId()); + // Use username for lookup since userId is often null in FIDO2 flows + Fido2UserMetrics userMetrics = getUserMetricsByUsername(request.getUsername()); if (userMetrics == null) { userMetrics = new Fido2UserMetrics(request.getUserId(), request.getUsername()); } @@ -118,7 +124,7 @@ public void updateUserAuthenticationMetrics(UserMetricsUpdateRequest request) { userMetrics.updateEngagementLevel(); saveUserMetrics(userMetrics); - log.debug("Updated user authentication metrics for user: {}", request.getUserId()); + log.debug("Updated user authentication metrics for username: {}", request.getUsername()); } catch (Exception e) { log.error("Failed to update user authentication metrics for user {}: {}", request.getUserId(), e.getMessage(), e); } @@ -135,7 +141,8 @@ public void updateUserFallbackMetrics(String userId, String username, String ipA CompletableFuture.runAsync(() -> { try { - Fido2UserMetrics userMetrics = getUserMetrics(userId); + // Use username for lookup since userId is often null in FIDO2 flows + Fido2UserMetrics userMetrics = getUserMetricsByUsername(username); if (userMetrics == null) { userMetrics = new Fido2UserMetrics(userId, username); } @@ -146,7 +153,7 @@ public void updateUserFallbackMetrics(String userId, String username, String ipA saveUserMetrics(userMetrics); - log.debug("Updated user fallback metrics for user: {}", userId); + log.debug("Updated user fallback metrics for username: {}", username); } catch (Exception e) { log.error("Failed to update user fallback metrics for user {}: {}", userId, e.getMessage(), e); } @@ -237,7 +244,11 @@ public List getUsersByAdoptionStage(String adoptionStage) { */ public List getNewUsers(int days) { try { - LocalDateTime cutoffDate = LocalDateTime.now().minusDays(days); + // Use UTC timezone to align with FIDO2 services + LocalDateTime cutoffLdt = ZonedDateTime.now(ZoneId.of("UTC")) + .toLocalDateTime() + .minusDays(days); + Date cutoffDate = Date.from(cutoffLdt.atZone(ZoneId.of("UTC")).toInstant()); Filter filter = Filter.createGreaterOrEqualFilter("jansFirstRegistrationDate", cutoffDate); return persistenceEntryManager.findEntries( USER_METRICS_BASE_DN, Fido2UserMetrics.class, filter @@ -350,10 +361,18 @@ public Map getUserBehaviorPatterns(String userId) { // Temporal patterns patterns.put("isNewUser", userMetrics.isNewUser()); patterns.put("isActiveUser", userMetrics.isActiveUser()); + // Use UTC timezone to align with FIDO2 services + LocalDateTime utcNow = ZonedDateTime.now(ZoneId.of("UTC")).toLocalDateTime(); patterns.put("daysSinceFirstRegistration", userMetrics.getFirstRegistrationDate() != null ? - java.time.temporal.ChronoUnit.DAYS.between(userMetrics.getFirstRegistrationDate(), LocalDateTime.now()) : 0); + java.time.temporal.ChronoUnit.DAYS.between( + LocalDateTime.ofInstant(userMetrics.getFirstRegistrationDate().toInstant(), ZoneId.of("UTC")), + utcNow + ) : 0); patterns.put("daysSinceLastActivity", userMetrics.getLastActivityDate() != null ? - java.time.temporal.ChronoUnit.DAYS.between(userMetrics.getLastActivityDate(), LocalDateTime.now()) : 0); + java.time.temporal.ChronoUnit.DAYS.between( + LocalDateTime.ofInstant(userMetrics.getLastActivityDate().toInstant(), ZoneId.of("UTC")), + utcNow + ) : 0); return patterns; } @@ -406,9 +425,15 @@ private boolean isFido2MetricsEnabled() { private void saveUserMetrics(Fido2UserMetrics userMetrics) { try { if (userMetrics.getDn() == null) { + // New entry - set DN and persist userMetrics.setDn(generateUserMetricsDn(userMetrics.getId())); + persistenceEntryManager.persist(userMetrics); + log.debug("Created new user metrics entry for: {}", userMetrics.getUsername()); + } else { + // Existing entry - merge to update + persistenceEntryManager.merge(userMetrics); + log.debug("Updated existing user metrics entry for: {}", userMetrics.getUsername()); } - persistenceEntryManager.persist(userMetrics); } catch (Exception e) { log.error("Failed to save user metrics: {}", e.getMessage(), e); } @@ -442,21 +467,21 @@ private void updateAverageDuration(Fido2UserMetrics userMetrics, Long durationMs if (isRegistration) { if (userMetrics.getAvgRegistrationDuration() == null) { - userMetrics.setAvgRegistrationDuration(durationMs.doubleValue()); + userMetrics.setAvgRegistrationDuration(durationMs); } else { // Simple moving average - in production, you might want more sophisticated calculation - double currentAvg = userMetrics.getAvgRegistrationDuration(); + long currentAvg = userMetrics.getAvgRegistrationDuration(); int count = userMetrics.getSuccessfulRegistrations() != null ? userMetrics.getSuccessfulRegistrations() : 1; - double newAvg = (currentAvg * (count - 1) + durationMs) / count; + long newAvg = (currentAvg * (count - 1) + durationMs) / count; userMetrics.setAvgRegistrationDuration(newAvg); } } else { if (userMetrics.getAvgAuthenticationDuration() == null) { - userMetrics.setAvgAuthenticationDuration(durationMs.doubleValue()); + userMetrics.setAvgAuthenticationDuration(durationMs); } else { - double currentAvg = userMetrics.getAvgAuthenticationDuration(); + long currentAvg = userMetrics.getAvgAuthenticationDuration(); int count = userMetrics.getSuccessfulAuthentications() != null ? userMetrics.getSuccessfulAuthentications() : 1; - double newAvg = (currentAvg * (count - 1) + durationMs) / count; + long newAvg = (currentAvg * (count - 1) + durationMs) / count; userMetrics.setAvgAuthenticationDuration(newAvg); } } diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/shared/LoggerService.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/shared/LoggerService.java index 0b5cb6a7761..622ac5d9704 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/shared/LoggerService.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/shared/LoggerService.java @@ -31,7 +31,6 @@ public boolean isDisableJdkLogger() { return appConfiguration.getDisableJdkLogger() != null && appConfiguration.getDisableJdkLogger(); } - @Override public boolean isDisableExternalLoggerConfiguration() { return isTrue(appConfiguration.getDisableExternalLoggerConfiguration()); } diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/shared/MetricService.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/shared/MetricService.java index 82e60de2fbe..10f023c7181 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/shared/MetricService.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/shared/MetricService.java @@ -14,6 +14,7 @@ import io.jans.fido2.model.conf.AppConfiguration; import io.jans.fido2.model.metric.Fido2MetricsData; import io.jans.fido2.model.metric.Fido2MetricType; +import io.jans.fido2.model.metric.UserMetricsUpdateRequest; import io.jans.fido2.service.util.DeviceInfoExtractor; import io.jans.model.ApplicationType; import io.jans.as.common.service.common.ApplicationFactory; @@ -25,6 +26,8 @@ import org.slf4j.Logger; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.concurrent.CompletableFuture; /** @@ -57,6 +60,10 @@ public class MetricService extends io.jans.service.metric.MetricService { @ReportMetric private PersistenceEntryManager persistenceEntryManager; + @Inject + @Named(ApplicationFactory.PERSISTENCE_ENTRY_MANAGER_NAME) + private PersistenceEntryManager userPersistenceEntryManager; + @Inject private DeviceInfoExtractor deviceInfoExtractor; @@ -65,7 +72,11 @@ public class MetricService extends io.jans.service.metric.MetricService { @Inject @Named("fido2MetricsService") - private io.jans.fido2.service.metric.Fido2MetricsService fido2MetricsService; + private Instance fido2MetricsServiceInstance; + + @Inject + @Named("fido2UserMetricsService") + private Instance fido2UserMetricsServiceInstance; private static final String UNKNOWN_ERROR = "UNKNOWN"; @@ -254,6 +265,11 @@ private void recordDetailedMetrics(String username, String status, HttpServletRe } storeFido2MetricsData(metricsData); + + // Update user-level metrics (skip for ATTEMPT status) + if (!ATTEMPT_STATUS.equals(status)) { + updateUserMetrics(metricsData); + } } } @@ -290,8 +306,10 @@ public void recordPasskeyFallback(String username, String fallbackMethod, String metricsData.setUsername(username); metricsData.setFallbackMethod(fallbackMethod); metricsData.setFallbackReason(reason); - metricsData.setStartTime(LocalDateTime.now()); - metricsData.setEndTime(LocalDateTime.now()); + // Use UTC timezone to align with FIDO2 services + LocalDateTime utcNow = ZonedDateTime.now(ZoneId.of("UTC")).toLocalDateTime(); + metricsData.setStartTime(utcNow); + metricsData.setEndTime(utcNow); storeFido2MetricsData(metricsData); } @@ -332,23 +350,59 @@ private Fido2MetricsData createMetricsData(String operationType, String username metricsData.setOperationType(operationType); metricsData.setOperationStatus(status); metricsData.setUsername(username); - metricsData.setStartTime(LocalDateTime.now()); - metricsData.setEndTime(LocalDateTime.now()); + + // Look up the real userId (inum) from username + // This is the immutable unique identifier that should be used for analytics + String userId = getUserIdFromUsername(username); + metricsData.setUserId(userId); + + // Use UTC timezone to align with FIDO2 services + LocalDateTime utcNow = ZonedDateTime.now(ZoneId.of("UTC")).toLocalDateTime(); + metricsData.setStartTime(utcNow); + metricsData.setEndTime(utcNow); if (authenticatorType != null) { metricsData.setAuthenticatorType(authenticatorType); incrementFido2Counter(Fido2MetricType.FIDO2_DEVICE_TYPE_USAGE); } - if (request != null && appConfiguration.isFido2DeviceInfoCollection()) { + // Extract HTTP request details + if (request != null) { try { - metricsData.setDeviceInfo(deviceInfoExtractor.extractDeviceInfo(request)); + // Extract IP address - check proxy headers first, then fall back to remote address + String ipAddress = extractIpAddress(request); + metricsData.setIpAddress(ipAddress); + + // Extract User-Agent header + String userAgent = request.getHeader("User-Agent"); + metricsData.setUserAgent(userAgent); } catch (Exception e) { - log.debug("Failed to extract device info: {}", e.getMessage()); - metricsData.setDeviceInfo(deviceInfoExtractor.createMinimalDeviceInfo()); + log.debug("Failed to extract request details: {}", e.getMessage()); + } + + // Extract device info if enabled + if (appConfiguration.isFido2DeviceInfoCollection()) { + try { + metricsData.setDeviceInfo(deviceInfoExtractor.extractDeviceInfo(request)); + } catch (Exception e) { + log.debug("Failed to extract device info: {}", e.getMessage()); + metricsData.setDeviceInfo(deviceInfoExtractor.createMinimalDeviceInfo()); + } } } + // Set node identifier (for cluster environments) - only if available + try { + String nodeId = networkService.getMacAdress(); + if (nodeId != null && !nodeId.trim().isEmpty()) { + metricsData.setNodeId(nodeId); + } + } catch (Exception e) { + log.debug("Failed to get node ID: {}", e.getMessage()); + } + + // Note: applicationType is not set as it's always "FIDO2" and redundant + return metricsData; } @@ -384,10 +438,10 @@ private String categorizeError(String errorReason) { */ private void storeFido2MetricsData(Fido2MetricsData metricsData) { try { - if (fido2MetricsService != null) { - fido2MetricsService.storeMetricsData(metricsData); + if (fido2MetricsServiceInstance != null && !fido2MetricsServiceInstance.isUnsatisfied()) { + fido2MetricsServiceInstance.get().storeMetricsData(metricsData); } else { - log.warn("Fido2MetricsService not available, cannot store metrics data"); + log.debug("Fido2MetricsService not available, skipping metrics data storage"); } } catch (Exception e) { log.error("Failed to store FIDO2 metrics data: {}", e.getMessage(), e); @@ -412,4 +466,152 @@ private void updateFido2Timer(Fido2MetricType fido2MetricType, long duration) { log.debug("FIDO2 Timer updated: {} - {} ms", fido2MetricType.getMetricName(), duration); } + /** + * Extract IP address from HTTP request, checking proxy headers first + * Handles X-Forwarded-For, Proxy-Client-IP, and other common proxy headers + * + * @param request HTTP servlet request + * @return Client IP address + */ + private String extractIpAddress(HttpServletRequest request) { + if (request == null) { + return null; + } + + // List of headers to check (in order of preference) + String[] headersToTry = { + "X-Forwarded-For", + "Proxy-Client-IP", + "WL-Proxy-Client-IP", + "HTTP_X_FORWARDED_FOR", + "HTTP_X_FORWARDED", + "HTTP_X_CLUSTER_CLIENT_IP", + "HTTP_CLIENT_IP", + "HTTP_FORWARDED_FOR", + "HTTP_FORWARDED", + "HTTP_VIA", + "REMOTE_ADDR" + }; + + // Check each header + for (String header : headersToTry) { + String ip = request.getHeader(header); + if (ip != null && !ip.trim().isEmpty() && !"unknown".equalsIgnoreCase(ip)) { + // X-Forwarded-For can contain multiple IPs; take the first one + int commaIndex = ip.indexOf(','); + if (commaIndex > 0) { + ip = ip.substring(0, commaIndex).trim(); + } + return ip; + } + } + + // Fallback to remote address + return request.getRemoteAddr(); + } + + /** + * Look up the immutable userId (inum) from username + * This ensures we use the stable unique identifier for analytics + * + * @param username The username to look up + * @return The user's inum (userId), or the username as fallback if lookup fails + */ + private String getUserIdFromUsername(String username) { + if (username == null || username.trim().isEmpty()) { + return null; + } + + try { + // In Janssen, users are stored with "uid" attribute as username + // and "inum" as the unique identifier + io.jans.orm.model.base.SimpleBranch usersBranch = new io.jans.orm.model.base.SimpleBranch(); + usersBranch.setDn(staticConfiguration.getBaseDn().getPeople()); + + // Search for user by uid (username) + io.jans.orm.search.filter.Filter filter = io.jans.orm.search.filter.Filter.createEqualityFilter("uid", username); + + // Use a simple user object to get the inum + java.util.List users = userPersistenceEntryManager.findEntries( + staticConfiguration.getBaseDn().getPeople(), + io.jans.as.common.model.common.User.class, + filter, + null, + 1 + ); + + if (users != null && !users.isEmpty()) { + io.jans.as.common.model.common.User user = users.get(0); + String inum = user.getAttribute("inum"); + if (inum != null && !inum.trim().isEmpty()) { + log.debug("Resolved username '{}' to userId '{}'", username, inum); + return inum; + } + } + + // Fallback: if we can't find the inum, use username as identifier + log.debug("Could not resolve userId for username '{}', using username as fallback", username); + return username; + + } catch (Exception e) { + // If lookup fails, use username as fallback (better than null) + log.debug("Failed to look up userId for username '{}': {}, using username as fallback", + username, e.getMessage()); + return username; + } + } + + /** + * Update user-level metrics based on the metrics data + */ + private void updateUserMetrics(Fido2MetricsData metricsData) { + if (fido2UserMetricsServiceInstance == null || fido2UserMetricsServiceInstance.isUnsatisfied()) { + log.debug("Fido2UserMetricsService not available, skipping user metrics update"); + return; + } + + try { + io.jans.fido2.service.metric.Fido2UserMetricsService userMetricsService = fido2UserMetricsServiceInstance.get(); + + UserMetricsUpdateRequest request = new UserMetricsUpdateRequest(); + request.setUserId(metricsData.getUserId()); + request.setUsername(metricsData.getUsername()); + request.setSuccess("SUCCESS".equals(metricsData.getOperationStatus())); + request.setAuthenticatorType(metricsData.getAuthenticatorType()); + request.setDurationMs(metricsData.getDurationMs()); + + // Extract device info if available + if (metricsData.getDeviceInfo() != null) { + request.setDeviceType(metricsData.getDeviceInfo().getDeviceType()); + request.setBrowser(metricsData.getDeviceInfo().getBrowser()); + request.setOs(metricsData.getDeviceInfo().getOperatingSystem()); + } + + request.setIpAddress(metricsData.getIpAddress()); + request.setUserAgent(metricsData.getUserAgent()); + request.setFallbackMethod(metricsData.getFallbackMethod()); + request.setFallbackReason(metricsData.getFallbackReason()); + + // Call the appropriate user metrics update method based on operation type + String operationType = metricsData.getOperationType(); + if ("REGISTRATION".equals(operationType)) { + userMetricsService.updateUserRegistrationMetrics(request); + log.debug("Updated user registration metrics for: {}", metricsData.getUsername()); + } else if ("AUTHENTICATION".equals(operationType)) { + userMetricsService.updateUserAuthenticationMetrics(request); + log.debug("Updated user authentication metrics for: {}", metricsData.getUsername()); + } else if ("FALLBACK".equals(operationType)) { + userMetricsService.updateUserFallbackMetrics( + request.getUserId(), + request.getUsername(), + request.getIpAddress(), + request.getUserAgent() + ); + log.debug("Updated user fallback metrics for: {}", metricsData.getUsername()); + } + } catch (Exception e) { + log.error("Failed to update user metrics: {}", e.getMessage(), e); + } + } + } \ No newline at end of file diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/ws/rs/controller/Fido2MetricsController.java b/jans-fido2/server/src/main/java/io/jans/fido2/ws/rs/controller/Fido2MetricsController.java new file mode 100644 index 00000000000..f8499298944 --- /dev/null +++ b/jans-fido2/server/src/main/java/io/jans/fido2/ws/rs/controller/Fido2MetricsController.java @@ -0,0 +1,483 @@ +/* + * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +package io.jans.fido2.ws.rs.controller; + +import io.jans.fido2.model.conf.AppConfiguration; +import io.jans.fido2.model.error.ErrorResponseFactory; +import io.jans.fido2.model.metric.Fido2MetricsConstants; +import io.jans.fido2.service.DataMapperService; +import io.jans.fido2.service.metric.Fido2MetricsService; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.slf4j.Logger; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * REST API controller for FIDO2/Passkey metrics + * Provides endpoints to fetch metrics data for dashboards and analytics tools + * + * GitHub Issue #11923 + * + * @author FIDO2 Team + */ +@ApplicationScoped +@Path("/metrics") +public class Fido2MetricsController { + + @Inject + private Logger log; + + @Inject + private Fido2MetricsService metricsService; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private DataMapperService dataMapperService; + + // ISO formatter for UTC timestamps (aligned with FIDO2 services) + private static final DateTimeFormatter ISO_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME; + + /** + * Get raw metrics entries within a time range + * + * @param startTime Start time in ISO format (e.g., 2024-01-01T00:00:00) + * @param endTime End time in ISO format + * @return List of metrics entries + */ + @GET + @Path("/entries") + @Produces(MediaType.APPLICATION_JSON) + public Response getMetricsEntries( + @QueryParam("startTime") String startTime, + @QueryParam("endTime") String endTime) { + return processRequest(() -> { + checkMetricsEnabled(); + + LocalDateTime start = parseDateTime(startTime, "startTime"); + LocalDateTime end = parseDateTime(endTime, "endTime"); + + List entries = metricsService.getMetricsEntries(start, end); + return Response.ok(dataMapperService.writeValueAsString(entries)).build(); + }); + } + + /** + * Get metrics entries for a specific user + * + * @param userId User ID + * @param startTime Start time in ISO format + * @param endTime End time in ISO format + * @return List of user-specific metrics entries + */ + @GET + @Path("/entries/user/{userId}") + @Produces(MediaType.APPLICATION_JSON) + public Response getMetricsEntriesByUser( + @PathParam("userId") String userId, + @QueryParam("startTime") String startTime, + @QueryParam("endTime") String endTime) { + return processRequest(() -> { + checkMetricsEnabled(); + + if (userId == null || userId.trim().isEmpty()) { + throw errorResponseFactory.invalidRequest("userId is required"); + } + + LocalDateTime start = parseDateTime(startTime, "startTime"); + LocalDateTime end = parseDateTime(endTime, "endTime"); + + List entries = metricsService.getMetricsEntriesByUser(userId, start, end); + return Response.ok(dataMapperService.writeValueAsString(entries)).build(); + }); + } + + /** + * Get metrics entries by operation type (REGISTRATION or AUTHENTICATION) + * + * @param operationType Operation type + * @param startTime Start time in ISO format + * @param endTime End time in ISO format + * @return List of operation-specific metrics entries + */ + @GET + @Path("/entries/operation/{operationType}") + @Produces(MediaType.APPLICATION_JSON) + public Response getMetricsEntriesByOperation( + @PathParam("operationType") String operationType, + @QueryParam("startTime") String startTime, + @QueryParam("endTime") String endTime) { + return processRequest(() -> { + checkMetricsEnabled(); + + if (operationType == null || operationType.trim().isEmpty()) { + throw errorResponseFactory.invalidRequest("operationType is required"); + } + + LocalDateTime start = parseDateTime(startTime, "startTime"); + LocalDateTime end = parseDateTime(endTime, "endTime"); + + List entries = metricsService.getMetricsEntriesByOperation(operationType, start, end); + return Response.ok(dataMapperService.writeValueAsString(entries)).build(); + }); + } + + /** + * Get aggregated metrics data + * + * @param aggregationType Aggregation type (HOURLY, DAILY, WEEKLY, MONTHLY) + * @param startTime Start time in ISO format + * @param endTime End time in ISO format + * @return List of aggregated metrics + */ + @GET + @Path("/aggregations/{aggregationType}") + @Produces(MediaType.APPLICATION_JSON) + public Response getAggregations( + @PathParam("aggregationType") String aggregationType, + @QueryParam("startTime") String startTime, + @QueryParam("endTime") String endTime) { + return processRequest(() -> { + checkMetricsEnabled(); + + validateAggregationType(aggregationType); + + LocalDateTime start = parseDateTime(startTime, "startTime"); + LocalDateTime end = parseDateTime(endTime, "endTime"); + + List aggregations = metricsService.getAggregations(aggregationType, start, end); + return Response.ok(dataMapperService.writeValueAsString(aggregations)).build(); + }); + } + + /** + * Get aggregation summary statistics + * + * @param aggregationType Aggregation type (HOURLY, DAILY, WEEKLY, MONTHLY) + * @param startTime Start time in ISO format + * @param endTime End time in ISO format + * @return Summary statistics + */ + @GET + @Path("/aggregations/{aggregationType}/summary") + @Produces(MediaType.APPLICATION_JSON) + public Response getAggregationSummary( + @PathParam("aggregationType") String aggregationType, + @QueryParam("startTime") String startTime, + @QueryParam("endTime") String endTime) { + return processRequest(() -> { + checkMetricsEnabled(); + + validateAggregationType(aggregationType); + + LocalDateTime start = parseDateTime(startTime, "startTime"); + LocalDateTime end = parseDateTime(endTime, "endTime"); + + Map summary = metricsService.getAggregationSummary(aggregationType, start, end); + return Response.ok(dataMapperService.writeValueAsString(summary)).build(); + }); + } + + /** + * Get user adoption metrics + * + * @param startTime Start time in ISO format + * @param endTime End time in ISO format + * @return User adoption statistics + */ + @GET + @Path("/analytics/adoption") + @Produces(MediaType.APPLICATION_JSON) + public Response getUserAdoptionMetrics( + @QueryParam("startTime") String startTime, + @QueryParam("endTime") String endTime) { + return processRequest(() -> { + checkMetricsEnabled(); + + LocalDateTime start = parseDateTime(startTime, "startTime"); + LocalDateTime end = parseDateTime(endTime, "endTime"); + + Map adoption = metricsService.getUserAdoptionMetrics(start, end); + return Response.ok(dataMapperService.writeValueAsString(adoption)).build(); + }); + } + + /** + * Get performance metrics (average durations, success rates) + * + * @param startTime Start time in ISO format + * @param endTime End time in ISO format + * @return Performance statistics + */ + @GET + @Path("/analytics/performance") + @Produces(MediaType.APPLICATION_JSON) + public Response getPerformanceMetrics( + @QueryParam("startTime") String startTime, + @QueryParam("endTime") String endTime) { + return processRequest(() -> { + checkMetricsEnabled(); + + LocalDateTime start = parseDateTime(startTime, "startTime"); + LocalDateTime end = parseDateTime(endTime, "endTime"); + + Map performance = metricsService.getPerformanceMetrics(start, end); + return Response.ok(dataMapperService.writeValueAsString(performance)).build(); + }); + } + + /** + * Get device analytics (platform distribution, authenticator types) + * + * @param startTime Start time in ISO format + * @param endTime End time in ISO format + * @return Device analytics data + */ + @GET + @Path("/analytics/devices") + @Produces(MediaType.APPLICATION_JSON) + public Response getDeviceAnalytics( + @QueryParam("startTime") String startTime, + @QueryParam("endTime") String endTime) { + return processRequest(() -> { + checkMetricsEnabled(); + + LocalDateTime start = parseDateTime(startTime, "startTime"); + LocalDateTime end = parseDateTime(endTime, "endTime"); + + Map devices = metricsService.getDeviceAnalytics(start, end); + return Response.ok(dataMapperService.writeValueAsString(devices)).build(); + }); + } + + /** + * Get error analysis (error categories, frequencies) + * + * @param startTime Start time in ISO format + * @param endTime End time in ISO format + * @return Error analysis data + */ + @GET + @Path("/analytics/errors") + @Produces(MediaType.APPLICATION_JSON) + public Response getErrorAnalysis( + @QueryParam("startTime") String startTime, + @QueryParam("endTime") String endTime) { + return processRequest(() -> { + checkMetricsEnabled(); + + LocalDateTime start = parseDateTime(startTime, "startTime"); + LocalDateTime end = parseDateTime(endTime, "endTime"); + + Map errors = metricsService.getErrorAnalysis(start, end); + return Response.ok(dataMapperService.writeValueAsString(errors)).build(); + }); + } + + /** + * Get trend analysis for metrics over time + * + * @param aggregationType Aggregation type for trend analysis + * @param startTime Start time in ISO format + * @param endTime End time in ISO format + * @return Trend analysis data + */ + @GET + @Path("/analytics/trends/{aggregationType}") + @Produces(MediaType.APPLICATION_JSON) + public Response getTrendAnalysis( + @PathParam("aggregationType") String aggregationType, + @QueryParam("startTime") String startTime, + @QueryParam("endTime") String endTime) { + return processRequest(() -> { + checkMetricsEnabled(); + + validateAggregationType(aggregationType); + + LocalDateTime start = parseDateTime(startTime, "startTime"); + LocalDateTime end = parseDateTime(endTime, "endTime"); + + Map trends = metricsService.getTrendAnalysis(aggregationType, start, end); + return Response.ok(dataMapperService.writeValueAsString(trends)).build(); + }); + } + + /** + * Get period-over-period comparison + * + * @param aggregationType Aggregation type for comparison + * @param periods Number of periods to compare (default: 2) + * @return Period comparison data + */ + @GET + @Path("/analytics/comparison/{aggregationType}") + @Produces(MediaType.APPLICATION_JSON) + public Response getPeriodOverPeriodComparison( + @PathParam("aggregationType") String aggregationType, + @QueryParam("periods") @DefaultValue("2") int periods) { + return processRequest(() -> { + checkMetricsEnabled(); + + validateAggregationType(aggregationType); + + if (periods < 2 || periods > 12) { + throw errorResponseFactory.invalidRequest("periods must be between 2 and 12"); + } + + Map comparison = metricsService.getPeriodOverPeriodComparison(aggregationType, periods); + return Response.ok(dataMapperService.writeValueAsString(comparison)).build(); + }); + } + + /** + * Get metrics configuration and status + * + * @return Configuration information + */ + @GET + @Path("/config") + @Produces(MediaType.APPLICATION_JSON) + public Response getMetricsConfig() { + return processRequest(() -> { + checkMetricsEnabled(); + + Map config = new HashMap<>(); + config.put("metricsEnabled", appConfiguration.isFido2MetricsEnabled()); + config.put("aggregationEnabled", appConfiguration.isFido2MetricsAggregationEnabled()); + config.put("retentionDays", appConfiguration.getFido2MetricsRetentionDays()); + config.put("deviceInfoCollection", appConfiguration.isFido2DeviceInfoCollection()); + config.put("errorCategorization", appConfiguration.isFido2ErrorCategorization()); + config.put("performanceMetrics", appConfiguration.isFido2PerformanceMetrics()); + config.put("supportedAggregationTypes", List.of( + Fido2MetricsConstants.HOURLY, + Fido2MetricsConstants.DAILY, + Fido2MetricsConstants.WEEKLY, + Fido2MetricsConstants.MONTHLY + )); + + return Response.ok(dataMapperService.writeValueAsString(config)).build(); + }); + } + + /** + * Health check endpoint for metrics service + * + * @return Health status + */ + @GET + @Path("/health") + @Produces(MediaType.APPLICATION_JSON) + public Response getHealth() { + return processRequest(() -> { + Map health = new HashMap<>(); + health.put("status", "UP"); + health.put("metricsEnabled", appConfiguration.isFido2MetricsEnabled()); + health.put("aggregationEnabled", appConfiguration.isFido2MetricsAggregationEnabled()); + health.put("timestamp", ZonedDateTime.now(ZoneId.of("UTC")).toLocalDateTime().format(ISO_FORMATTER)); + + return Response.ok(dataMapperService.writeValueAsString(health)).build(); + }); + } + + // ========== HELPER METHODS ========== + + /** + * Check if FIDO2 metrics are enabled + * @throws WebApplicationException if metrics are disabled + */ + private void checkMetricsEnabled() { + if (appConfiguration.getFido2Configuration() == null) { + throw errorResponseFactory.forbiddenException(); + } + + if (!appConfiguration.isFido2MetricsEnabled()) { + throw errorResponseFactory.forbiddenException(); + } + } + + /** + * Parse ISO datetime string as UTC + * Note: All timestamps are interpreted as UTC to align with FIDO2 services + * @param dateTime DateTime string in ISO format (interpreted as UTC) + * @param paramName Parameter name for error messages + * @return Parsed LocalDateTime in UTC + */ + private LocalDateTime parseDateTime(String dateTime, String paramName) { + if (dateTime == null || dateTime.trim().isEmpty()) { + throw errorResponseFactory.invalidRequest(paramName + " is required (ISO format: yyyy-MM-ddTHH:mm:ss in UTC)"); + } + + try { + // Parse as LocalDateTime - all times are assumed to be UTC + return LocalDateTime.parse(dateTime, ISO_FORMATTER); + } catch (Exception e) { + throw errorResponseFactory.invalidRequest( + paramName + " must be in ISO format (yyyy-MM-ddTHH:mm:ss in UTC). Example: 2024-01-01T00:00:00" + ); + } + } + + /** + * Validate aggregation type parameter + * @param aggregationType Aggregation type to validate + * @throws WebApplicationException if invalid + */ + private void validateAggregationType(String aggregationType) { + if (aggregationType == null || aggregationType.trim().isEmpty()) { + throw errorResponseFactory.invalidRequest("aggregationType is required"); + } + + String upperType = aggregationType.toUpperCase(); + if (!Fido2MetricsConstants.HOURLY.equals(upperType) && + !Fido2MetricsConstants.DAILY.equals(upperType) && + !Fido2MetricsConstants.WEEKLY.equals(upperType) && + !Fido2MetricsConstants.MONTHLY.equals(upperType)) { + throw errorResponseFactory.invalidRequest( + "aggregationType must be one of: HOURLY, DAILY, WEEKLY, MONTHLY" + ); + } + } + + /** + * Process REST request with error handling + * @param processor Request processor + * @return Response + */ + private Response processRequest(RequestProcessor processor) { + try { + return processor.process(); + } catch (WebApplicationException e) { + throw e; + } catch (Exception e) { + log.error("Error processing metrics request: {}", e.getMessage(), e); + throw errorResponseFactory.unknownError(e.getMessage()); + } + } + + /** + * Functional interface for request processing + */ + @FunctionalInterface + private interface RequestProcessor { + Response process() throws Exception; + } +} + diff --git a/jans-fido2/server/src/main/resources/fido2-metrics.properties b/jans-fido2/server/src/main/resources/fido2-metrics.properties index 90d868cc05e..42f6e02a3c7 100644 --- a/jans-fido2/server/src/main/resources/fido2-metrics.properties +++ b/jans-fido2/server/src/main/resources/fido2-metrics.properties @@ -1,107 +1,32 @@ -# FIDO2 Metrics Configuration Properties -# This file contains configurable constants for FIDO2 metrics system +# FIDO2 Metrics Aggregation Configuration +# Cron expressions use Quartz format: second minute hour day-of-month month day-of-week -# Base DN for FIDO2 metrics entries -fido2.metrics.base.dn=ou=fido2-metrics,o=jans - -# Base DN for FIDO2 user metrics -fido2.user.metrics.base.dn=ou=fido2-user-metrics,o=jans - -# Base DN for FIDO2 metrics aggregation -fido2.aggregation.base.dn=ou=fido2-aggregation,o=jans - -# Thresholds for user behavior analysis -fido2.user.high.fallback.rate.threshold=0.5 -fido2.user.medium.fallback.rate.threshold=0.3 -fido2.user.low.fallback.rate.threshold=0.1 - -# Engagement level thresholds -fido2.user.high.engagement.min.operations=50 -fido2.user.medium.engagement.min.operations=20 -fido2.user.low.engagement.min.operations=5 - -# Adoption stage thresholds -fido2.user.early.adoption.max.days=30 -fido2.user.growth.adoption.max.days=90 -fido2.user.mature.adoption.min.days=90 - -# Performance thresholds -fido2.performance.slow.operation.threshold.ms=5000 -fido2.performance.very.slow.operation.threshold.ms=10000 - -# Risk score thresholds -fido2.user.risk.high.threshold=0.8 -fido2.user.risk.medium.threshold=0.5 -fido2.user.risk.low.threshold=0.2 - -# Risk calculation factors -fido2.user.risk.high.fallback.factor=0.3 -fido2.user.risk.medium.fallback.factor=0.2 -fido2.user.risk.low.success.factor=0.1 -fido2.user.risk.medium.success.factor=0.05 -fido2.user.risk.new.user.factor=0.15 -fido2.user.risk.inactive.user.factor=0.1 -fido2.user.risk.max.score=1.0 - -# Success rate thresholds -fido2.user.success.rate.low.threshold=0.8 -fido2.user.success.rate.medium.threshold=0.9 - -# Aggregation intervals -fido2.aggregation.hourly.interval.minutes=60 -fido2.aggregation.daily.interval.hours=24 -fido2.aggregation.weekly.interval.days=7 -fido2.aggregation.monthly.interval.days=30 - -# Data retention -fido2.data.retention.days=30 -fido2.aggregation.retention.days=365 - -# ========== ENHANCED AGGREGATION CONFIGURATION (GitHub Issue #4) ========== - -# Aggregation Type Enablement +# Hourly Aggregation +# Runs at 5 minutes past every hour fido2.metrics.aggregation.hourly.enabled=true -fido2.metrics.aggregation.daily.enabled=true -fido2.metrics.aggregation.weekly.enabled=true -fido2.metrics.aggregation.monthly.enabled=true - -# Aggregation Scheduling (Cron expressions) -fido2.metrics.aggregation.hourly.cron=0 5 * * * * -fido2.metrics.aggregation.daily.cron=0 10 1 * * * -fido2.metrics.aggregation.weekly.cron=0 15 1 * * MON -fido2.metrics.aggregation.monthly.cron=0 20 1 1 * * +fido2.metrics.aggregation.hourly.cron=0 5 * * * ? -# Retention Configuration (in days) -fido2.metrics.aggregation.retention.hourly.days=7 -fido2.metrics.aggregation.retention.daily.days=90 -fido2.metrics.aggregation.retention.weekly.days=365 -fido2.metrics.aggregation.retention.monthly.days=1095 +# Daily Aggregation +# Runs at 1:10 AM every day +fido2.metrics.aggregation.daily.enabled=true +fido2.metrics.aggregation.daily.cron=0 10 1 * * ? -# Performance Configuration -fido2.metrics.aggregation.batch.size=1000 -fido2.metrics.aggregation.parallel.processing=true -fido2.metrics.aggregation.max.concurrent.threads=4 -fido2.metrics.aggregation.timeout.minutes=30 +# Weekly Aggregation +# Runs at 1:15 AM every Monday +fido2.metrics.aggregation.weekly.enabled=true +fido2.metrics.aggregation.weekly.cron=0 15 1 ? * MON -# Trend Analysis Configuration -fido2.metrics.trend.analysis.enabled=true -fido2.metrics.trend.growth.threshold.percent=5.0 -fido2.metrics.trend.stable.threshold.percent=5.0 -fido2.metrics.trend.analysis.window.days=30 +# Monthly Aggregation +# Runs at 1:20 AM on the 1st day of every month +fido2.metrics.aggregation.monthly.enabled=true +fido2.metrics.aggregation.monthly.cron=0 20 1 1 * ? -# Period Comparison Configuration -fido2.metrics.period.comparison.enabled=true -fido2.metrics.period.comparison.default.periods=7 -fido2.metrics.period.comparison.max.periods=90 +# Data Retention (in days) +fido2.metrics.retention.entries=90 +fido2.metrics.retention.aggregations=365 +fido2.metrics.retention.userMetrics=730 -# Batch processing -fido2.batch.size=1000 -fido2.batch.timeout.seconds=300 +# Performance Settings +fido2.metrics.async.enabled=true +fido2.metrics.batch.size=1000 -# Error categorization -fido2.error.timeout.keywords=timeout,expired,time.out -fido2.error.invalid.keywords=invalid,malformed,bad.format -fido2.error.notfound.keywords=not.found,missing,not.exist -fido2.error.auth.keywords=unauthorized,forbidden,access.denied -fido2.error.server.keywords=server,internal,service.unavailable -fido2.error.network.keywords=network,connection,unreachable diff --git a/jans-fido2/server/src/test/java/io/jans/fido2/service/metric/Fido2MetricsTrendAnalysisTest.java b/jans-fido2/server/src/test/java/io/jans/fido2/service/metric/Fido2MetricsTrendAnalysisTest.java deleted file mode 100644 index d4ff344c70e..00000000000 --- a/jans-fido2/server/src/test/java/io/jans/fido2/service/metric/Fido2MetricsTrendAnalysisTest.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. - * - * Copyright (c) 2020, Janssen Project - */ - -package io.jans.fido2.service.metric; - -import io.jans.fido2.model.metric.Fido2MetricsAggregation; -import io.jans.fido2.model.metric.Fido2MetricsConstants; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.slf4j.Logger; - -import java.time.LocalDateTime; -import java.util.*; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - -/** - * Test class for FIDO2 Metrics Trend Analysis functionality (GitHub Issue #11923) - * - * @author FIDO2 Team - */ -@ExtendWith(MockitoExtension.class) -class Fido2MetricsTrendAnalysisTest { - - @Mock - private Logger log; - - @Mock - private io.jans.fido2.model.conf.AppConfiguration appConfiguration; - - @Mock - private io.jans.orm.PersistenceEntryManager persistenceEntryManager; - - @InjectMocks - private Fido2MetricsService metricsService; - - private List testAggregations; - - @BeforeEach - void setUp() { - // Create test aggregations with increasing trend - testAggregations = new ArrayList<>(); - - for (int i = 0; i < 5; i++) { - Fido2MetricsAggregation aggregation = new Fido2MetricsAggregation("DAILY", "2024-01-0" + (i + 1), - LocalDateTime.now().minusDays(4 - i), LocalDateTime.now().minusDays(3 - i)); - - Map metricsData = new HashMap<>(); - metricsData.put(Fido2MetricsConstants.REGISTRATION_ATTEMPTS, 100L + (i * 20)); - metricsData.put(Fido2MetricsConstants.REGISTRATION_SUCCESSES, 95L + (i * 18)); - metricsData.put(Fido2MetricsConstants.AUTHENTICATION_ATTEMPTS, 500L + (i * 50)); - metricsData.put(Fido2MetricsConstants.AUTHENTICATION_SUCCESSES, 480L + (i * 45)); - metricsData.put(Fido2MetricsConstants.FALLBACK_EVENTS, 5L + i); - metricsData.put(Fido2MetricsConstants.REGISTRATION_SUCCESS_RATE, 0.95 + (i * 0.01)); - metricsData.put(Fido2MetricsConstants.AUTHENTICATION_SUCCESS_RATE, 0.96 + (i * 0.005)); - - aggregation.setMetricsData(metricsData); - testAggregations.add(aggregation); - } - } - - @Test - void testGetTrendAnalysis_WithValidData_ReturnsCorrectTrend() { - // Mock the getAggregations method to return test data - when(persistenceEntryManager.findEntries(anyString(), eq(Fido2MetricsAggregation.class), any())) - .thenReturn(testAggregations); - - // Execute trend analysis - Map result = metricsService.getTrendAnalysis("DAILY", - LocalDateTime.now().minusDays(5), LocalDateTime.now()); - - // Verify results - assertNotNull(result); - assertTrue(result.containsKey("dataPoints")); - assertTrue(result.containsKey("growthRate")); - assertTrue(result.containsKey("trendDirection")); - assertTrue(result.containsKey("insights")); - - // Verify trend direction is INCREASING (since we have increasing data) - assertEquals(Fido2MetricsConstants.INCREASING, result.get("trendDirection")); - - // Verify growth rate is positive - Double growthRate = (Double) result.get("growthRate"); - assertTrue(growthRate > 0); - } - - @Test - void testGetTrendAnalysis_WithEmptyData_ReturnsEmptyMap() { - // Mock empty result - when(persistenceEntryManager.findEntries(anyString(), eq(Fido2MetricsAggregation.class), any())) - .thenReturn(Collections.emptyList()); - - // Execute trend analysis - Map result = metricsService.getTrendAnalysis("DAILY", - LocalDateTime.now().minusDays(5), LocalDateTime.now()); - - // Verify empty result - assertTrue(result.isEmpty()); - } - - @Test - void testGetPeriodOverPeriodComparison_WithValidData_ReturnsComparison() { - // Mock current and previous period data - List currentPeriod = testAggregations.subList(0, 3); - List previousPeriod = testAggregations.subList(2, 5); - - when(persistenceEntryManager.findEntries(anyString(), eq(Fido2MetricsAggregation.class), any())) - .thenReturn(currentPeriod) - .thenReturn(previousPeriod); - - // Execute period comparison - Map result = metricsService.getPeriodOverPeriodComparison("DAILY", 3); - - // Verify results - assertNotNull(result); - assertTrue(result.containsKey(Fido2MetricsConstants.CURRENT_PERIOD)); - assertTrue(result.containsKey(Fido2MetricsConstants.PREVIOUS_PERIOD)); - assertTrue(result.containsKey(Fido2MetricsConstants.COMPARISON)); - - // Verify comparison contains change percentage - Map comparison = (Map) result.get(Fido2MetricsConstants.COMPARISON); - assertTrue(comparison.containsKey("totalOperationsChange")); - } - - @Test - void testGetAggregationSummary_WithValidData_ReturnsSummary() { - // Mock aggregation data - when(persistenceEntryManager.findEntries(anyString(), eq(Fido2MetricsAggregation.class), any())) - .thenReturn(testAggregations); - - // Execute summary - Map result = metricsService.getAggregationSummary("DAILY", - LocalDateTime.now().minusDays(5), LocalDateTime.now()); - - // Verify results - assertNotNull(result); - assertTrue(result.containsKey("totalRegistrations")); - assertTrue(result.containsKey("totalAuthentications")); - assertTrue(result.containsKey("totalFallbacks")); - assertTrue(result.containsKey("totalOperations")); - assertTrue(result.containsKey("avgRegistrationSuccessRate")); - assertTrue(result.containsKey("avgAuthenticationSuccessRate")); - - // Verify totals are calculated correctly - Long totalRegistrations = (Long) result.get("totalRegistrations"); - Long totalAuthentications = (Long) result.get("totalAuthentications"); - Long totalOperations = (Long) result.get("totalOperations"); - - assertEquals(totalRegistrations + totalAuthentications, totalOperations); - } - - @Test - void testGetTrendAnalysis_WithException_ReturnsEmptyMap() { - // Mock exception - when(persistenceEntryManager.findEntries(anyString(), eq(Fido2MetricsAggregation.class), any())) - .thenThrow(new RuntimeException("Database error")); - - // Execute trend analysis - Map result = metricsService.getTrendAnalysis("DAILY", - LocalDateTime.now().minusDays(5), LocalDateTime.now()); - - // Verify empty result on exception - assertTrue(result.isEmpty()); - } - - @Test - void testTrendAnalysis_WithStableData_ReturnsStableTrend() { - // Create stable data (same values) - List stableAggregations = new ArrayList<>(); - - for (int i = 0; i < 3; i++) { - Fido2MetricsAggregation aggregation = new Fido2MetricsAggregation("DAILY", "2024-01-0" + (i + 1), - LocalDateTime.now().minusDays(2 - i), LocalDateTime.now().minusDays(1 - i)); - - Map metricsData = new HashMap<>(); - metricsData.put(Fido2MetricsConstants.REGISTRATION_ATTEMPTS, 100L); - metricsData.put(Fido2MetricsConstants.AUTHENTICATION_ATTEMPTS, 500L); - aggregation.setMetricsData(metricsData); - stableAggregations.add(aggregation); - } - - when(persistenceEntryManager.findEntries(anyString(), eq(Fido2MetricsAggregation.class), any())) - .thenReturn(stableAggregations); - - // Execute trend analysis - Map result = metricsService.getTrendAnalysis("DAILY", - LocalDateTime.now().minusDays(3), LocalDateTime.now()); - - // Verify stable trend - assertEquals(Fido2MetricsConstants.STABLE, result.get("trendDirection")); - assertEquals(0.0, (Double) result.get("growthRate"), 0.01); - } -} - From 4b31356724bdb945c005b34f0d39f0003473a55c Mon Sep 17 00:00:00 2001 From: imran Date: Mon, 5 Jan 2026 16:15:33 +0500 Subject: [PATCH 2/2] feat(jans-fido2): resolved sonar cube issues Signed-off-by: imran --- .../model/metric/Fido2MetricsConstants.java | 4 + .../fido2/model/metric/Fido2MetricsEntry.java | 1 - .../fido2/model/metric/Fido2UserMetrics.java | 1 - .../Fido2MetricsAggregationScheduler.java | 4 +- .../service/metric/Fido2MetricsService.java | 175 ++++++++++-------- .../fido2/service/shared/MetricService.java | 7 +- .../rs/controller/Fido2MetricsController.java | 46 ++--- 7 files changed, 131 insertions(+), 107 deletions(-) diff --git a/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2MetricsConstants.java b/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2MetricsConstants.java index 625e1516db3..2c9a48742c6 100644 --- a/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2MetricsConstants.java +++ b/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2MetricsConstants.java @@ -108,6 +108,10 @@ private Fido2MetricsConstants() { public static final String PREVIOUS_PERIOD = "previousPeriod"; public static final String COMPARISON = "comparison"; + // Query parameter names + public static final String PARAM_START_TIME = "startTime"; + public static final String PARAM_END_TIME = "endTime"; + // Fallback method constants public static final String FALLBACK_METHOD_PASSWORD = "PASSWORD"; diff --git a/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2MetricsEntry.java b/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2MetricsEntry.java index 3001c862e1c..3c2dfdd29d2 100644 --- a/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2MetricsEntry.java +++ b/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2MetricsEntry.java @@ -16,7 +16,6 @@ import io.jans.orm.model.base.Entry; import java.io.Serializable; -import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.Date; diff --git a/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2UserMetrics.java b/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2UserMetrics.java index 8a58999372f..b4a4bd9e102 100644 --- a/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2UserMetrics.java +++ b/jans-fido2/model/src/main/java/io/jans/fido2/model/metric/Fido2UserMetrics.java @@ -14,7 +14,6 @@ import io.jans.orm.model.base.Entry; import java.io.Serializable; -import java.time.LocalDateTime; import java.util.Date; import java.util.Map; import java.util.Objects; diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/metric/Fido2MetricsAggregationScheduler.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/metric/Fido2MetricsAggregationScheduler.java index 871ef9ff9fa..a80744d3f0e 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/metric/Fido2MetricsAggregationScheduler.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/metric/Fido2MetricsAggregationScheduler.java @@ -78,7 +78,9 @@ public class Fido2MetricsAggregationScheduler { bundle = ResourceBundle.getBundle("fido2-metrics"); } catch (Exception e) { // Properties file not found - will use hardcoded defaults - System.err.println("WARN: fido2-metrics.properties not found, using defaults"); + // Use logger factory since instance logger not available in static context + LoggerFactory.getLogger(Fido2MetricsAggregationScheduler.class) + .warn("fido2-metrics.properties not found, using defaults"); } METRICS_CONFIG = bundle; } diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/metric/Fido2MetricsService.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/metric/Fido2MetricsService.java index bd8e70820c3..1b2c2cf2b3a 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/metric/Fido2MetricsService.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/metric/Fido2MetricsService.java @@ -89,8 +89,8 @@ public void storeMetricsData(Fido2MetricsData metricsData) { public List getMetricsEntries(LocalDateTime startTime, LocalDateTime endTime) { try { // Convert LocalDateTime to Date for SQL persistence filters - Date startDate = Date.from(startTime.atZone(ZoneId.of("UTC")).toInstant()); - Date endDate = Date.from(endTime.atZone(ZoneId.of("UTC")).toInstant()); + Date startDate = convertToDate(startTime); + Date endDate = convertToDate(endTime); Filter filter = Filter.createANDFilter( Filter.createGreaterOrEqualFilter(Fido2MetricsConstants.JANS_TIMESTAMP, startDate), @@ -114,8 +114,8 @@ public List getMetricsEntries(LocalDateTime startTime, LocalD public List getMetricsEntriesByUser(String userId, LocalDateTime startTime, LocalDateTime endTime) { try { // Convert LocalDateTime to Date for SQL persistence filters - Date startDate = Date.from(startTime.atZone(ZoneId.of("UTC")).toInstant()); - Date endDate = Date.from(endTime.atZone(ZoneId.of("UTC")).toInstant()); + Date startDate = convertToDate(startTime); + Date endDate = convertToDate(endTime); Filter filter = Filter.createANDFilter( Filter.createEqualityFilter("jansFido2MetricsUserId", userId), @@ -138,8 +138,8 @@ public List getMetricsEntriesByUser(String userId, LocalDateT public List getMetricsEntriesByOperation(String operationType, LocalDateTime startTime, LocalDateTime endTime) { try { // Convert LocalDateTime to Date for SQL persistence filters - Date startDate = Date.from(startTime.atZone(ZoneId.of("UTC")).toInstant()); - Date endDate = Date.from(endTime.atZone(ZoneId.of("UTC")).toInstant()); + Date startDate = convertToDate(startTime); + Date endDate = convertToDate(endTime); Filter filter = Filter.createANDFilter( Filter.createEqualityFilter("jansFido2MetricsOperationType", operationType), @@ -290,8 +290,8 @@ public void createMonthlyAggregation(LocalDateTime month) { public List getAggregations(String aggregationType, LocalDateTime startTime, LocalDateTime endTime) { try { // Convert LocalDateTime to Date for SQL persistence filters - Date startDate = Date.from(startTime.atZone(ZoneId.of("UTC")).toInstant()); - Date endDate = Date.from(endTime.atZone(ZoneId.of("UTC")).toInstant()); + Date startDate = convertToDate(startTime); + Date endDate = convertToDate(endTime); // Interval overlap check: aggregation overlaps query if: // aggregation.startTime <= queryEndTime AND aggregation.endTime >= queryStartTime @@ -628,8 +628,8 @@ private Fido2MetricsAggregation calculateAggregation(String aggregationType, Str private List getMetricsEntriesByTimeRange(LocalDateTime startTime, LocalDateTime endTime) { try { // Convert LocalDateTime to Date for SQL persistence filters - Date startDate = Date.from(startTime.atZone(ZoneId.of("UTC")).toInstant()); - Date endDate = Date.from(endTime.atZone(ZoneId.of("UTC")).toInstant()); + Date startDate = convertToDate(startTime); + Date endDate = convertToDate(endTime); Filter filter = Filter.createANDFilter( Filter.createGreaterOrEqualFilter(Fido2MetricsConstants.JANS_TIMESTAMP, startDate), @@ -644,6 +644,13 @@ private List getMetricsEntriesByTimeRange(LocalDateTime start return Collections.emptyList(); } } + + /** + * Convert LocalDateTime to Date for persistence layer compatibility + */ + private Date convertToDate(LocalDateTime dateTime) { + return Date.from(dateTime.atZone(ZoneId.of("UTC")).toInstant()); + } // ========== HELPER METHODS ========== @@ -665,96 +672,104 @@ private Fido2MetricsEntry convertToMetricsEntry(Fido2MetricsData metricsData) { // Convert LocalDateTime to Date for ORM compatibility (already in UTC) if (metricsData.getTimestamp() != null) { - entry.setTimestamp(Date.from(metricsData.getTimestamp().atZone(ZoneId.of("UTC")).toInstant())); + entry.setTimestamp(convertToDate(metricsData.getTimestamp())); } // Essential fields - always set + setEssentialFields(entry, metricsData); + + // Optional fields - only set if available + setOptionalFields(entry, metricsData); + + // Device info - only set if available and non-empty + setDeviceInfo(entry, metricsData); + + return entry; + } + + /** + * Set essential fields that are always present + */ + private void setEssentialFields(Fido2MetricsEntry entry, Fido2MetricsData metricsData) { entry.setUserId(metricsData.getUserId()); entry.setUsername(metricsData.getUsername()); entry.setOperationType(metricsData.getOperationType()); entry.setStatus(metricsData.getOperationStatus()); - - // Performance metrics - only set if available + } + + /** + * Set optional fields that may be null or empty + */ + private void setOptionalFields(Fido2MetricsEntry entry, Fido2MetricsData metricsData) { + // Performance metrics if (metricsData.getDurationMs() != null) { entry.setDurationMs(metricsData.getDurationMs()); } - // Authenticator info - only set if available - if (metricsData.getAuthenticatorType() != null && !metricsData.getAuthenticatorType().trim().isEmpty()) { - entry.setAuthenticatorType(metricsData.getAuthenticatorType()); - } + // Authenticator info + setIfNotEmpty(metricsData.getAuthenticatorType(), entry::setAuthenticatorType); - // Error info - only set for failures - if (metricsData.getErrorReason() != null && !metricsData.getErrorReason().trim().isEmpty()) { - entry.setErrorReason(metricsData.getErrorReason()); - } - if (metricsData.getErrorCategory() != null && !metricsData.getErrorCategory().trim().isEmpty()) { - entry.setErrorCategory(metricsData.getErrorCategory()); - } + // Error info + setIfNotEmpty(metricsData.getErrorReason(), entry::setErrorReason); + setIfNotEmpty(metricsData.getErrorCategory(), entry::setErrorCategory); - // Fallback info - only set for fallback events - if (metricsData.getFallbackMethod() != null && !metricsData.getFallbackMethod().trim().isEmpty()) { - entry.setFallbackMethod(metricsData.getFallbackMethod()); - } - if (metricsData.getFallbackReason() != null && !metricsData.getFallbackReason().trim().isEmpty()) { - entry.setFallbackReason(metricsData.getFallbackReason()); - } + // Fallback info + setIfNotEmpty(metricsData.getFallbackMethod(), entry::setFallbackMethod); + setIfNotEmpty(metricsData.getFallbackReason(), entry::setFallbackReason); + + // Network info + setIfNotEmpty(metricsData.getIpAddress(), entry::setIpAddress); + setIfNotEmpty(metricsData.getUserAgent(), entry::setUserAgent); - // Network info - only set if available - if (metricsData.getIpAddress() != null && !metricsData.getIpAddress().trim().isEmpty()) { - entry.setIpAddress(metricsData.getIpAddress()); + // Session info + setIfNotEmpty(metricsData.getSessionId(), entry::setSessionId); + + // Cluster info + setIfNotEmpty(metricsData.getNodeId(), entry::setNodeId); + } + + /** + * Set field value if string is not null and not empty + */ + private void setIfNotEmpty(String value, java.util.function.Consumer setter) { + if (value != null && !value.trim().isEmpty()) { + setter.accept(value); } - if (metricsData.getUserAgent() != null && !metricsData.getUserAgent().trim().isEmpty()) { - entry.setUserAgent(metricsData.getUserAgent()); + } + + /** + * Set device info if available and non-empty + */ + private void setDeviceInfo(Fido2MetricsEntry entry, Fido2MetricsData metricsData) { + if (metricsData.getDeviceInfo() == null) { + return; } - // Session info - only set if available - if (metricsData.getSessionId() != null && !metricsData.getSessionId().trim().isEmpty()) { - entry.setSessionId(metricsData.getSessionId()); - } + Fido2MetricsEntry.DeviceInfo deviceInfo = new Fido2MetricsEntry.DeviceInfo(); + boolean hasDeviceInfo = false; - // Cluster info - only set if available (useful for multi-node deployments) - if (metricsData.getNodeId() != null && !metricsData.getNodeId().trim().isEmpty()) { - entry.setNodeId(metricsData.getNodeId()); - } + hasDeviceInfo |= setDeviceField(metricsData.getDeviceInfo().getBrowser(), deviceInfo::setBrowser); + hasDeviceInfo |= setDeviceField(metricsData.getDeviceInfo().getBrowserVersion(), deviceInfo::setBrowserVersion); + hasDeviceInfo |= setDeviceField(metricsData.getDeviceInfo().getOperatingSystem(), deviceInfo::setOs); + hasDeviceInfo |= setDeviceField(metricsData.getDeviceInfo().getOsVersion(), deviceInfo::setOsVersion); + hasDeviceInfo |= setDeviceField(metricsData.getDeviceInfo().getDeviceType(), deviceInfo::setDeviceType); + hasDeviceInfo |= setDeviceField(metricsData.getDeviceInfo().getUserAgent(), deviceInfo::setUserAgent); - // Convert device info - only set if available and non-empty - if (metricsData.getDeviceInfo() != null) { - Fido2MetricsEntry.DeviceInfo deviceInfo = new Fido2MetricsEntry.DeviceInfo(); - boolean hasDeviceInfo = false; - - if (metricsData.getDeviceInfo().getBrowser() != null && !metricsData.getDeviceInfo().getBrowser().trim().isEmpty()) { - deviceInfo.setBrowser(metricsData.getDeviceInfo().getBrowser()); - hasDeviceInfo = true; - } - if (metricsData.getDeviceInfo().getBrowserVersion() != null && !metricsData.getDeviceInfo().getBrowserVersion().trim().isEmpty()) { - deviceInfo.setBrowserVersion(metricsData.getDeviceInfo().getBrowserVersion()); - hasDeviceInfo = true; - } - if (metricsData.getDeviceInfo().getOperatingSystem() != null && !metricsData.getDeviceInfo().getOperatingSystem().trim().isEmpty()) { - deviceInfo.setOs(metricsData.getDeviceInfo().getOperatingSystem()); - hasDeviceInfo = true; - } - if (metricsData.getDeviceInfo().getOsVersion() != null && !metricsData.getDeviceInfo().getOsVersion().trim().isEmpty()) { - deviceInfo.setOsVersion(metricsData.getDeviceInfo().getOsVersion()); - hasDeviceInfo = true; - } - if (metricsData.getDeviceInfo().getDeviceType() != null && !metricsData.getDeviceInfo().getDeviceType().trim().isEmpty()) { - deviceInfo.setDeviceType(metricsData.getDeviceInfo().getDeviceType()); - hasDeviceInfo = true; - } - if (metricsData.getDeviceInfo().getUserAgent() != null && !metricsData.getDeviceInfo().getUserAgent().trim().isEmpty()) { - deviceInfo.setUserAgent(metricsData.getDeviceInfo().getUserAgent()); - hasDeviceInfo = true; - } - - // Only set deviceInfo if we have at least one field populated - if (hasDeviceInfo) { - entry.setDeviceInfo(deviceInfo); - } + if (hasDeviceInfo) { + entry.setDeviceInfo(deviceInfo); } - - return entry; + } + + /** + * Set device field if value is not null and not empty + * @return true if field was set, false otherwise + */ + private boolean setDeviceField(String value, java.util.function.Consumer setter) { + if (value != null && !value.trim().isEmpty()) { + setter.accept(value); + return true; + } + return false; } // ========== TREND ANALYSIS METHODS (GitHub Issue #4) ========== diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/service/shared/MetricService.java b/jans-fido2/server/src/main/java/io/jans/fido2/service/shared/MetricService.java index 10f023c7181..4d6866e9477 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/service/shared/MetricService.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/service/shared/MetricService.java @@ -81,6 +81,7 @@ public class MetricService extends io.jans.service.metric.MetricService { private static final String UNKNOWN_ERROR = "UNKNOWN"; private static final String ATTEMPT_STATUS = "ATTEMPT"; + private static final String SUCCESS_STATUS = "SUCCESS"; public void initTimer() { initTimer(this.appConfiguration.getMetricReporterInterval(), this.appConfiguration.getMetricReporterKeepDataDays()); @@ -137,7 +138,7 @@ public void recordPasskeyRegistrationAttempt(String username, HttpServletRequest * @param authenticatorType Type of authenticator used */ public void recordPasskeyRegistrationSuccess(String username, HttpServletRequest request, long startTime, String authenticatorType) { - recordRegistrationMetrics(username, request, startTime, authenticatorType, "SUCCESS", null, Fido2MetricType.FIDO2_REGISTRATION_SUCCESS); + recordRegistrationMetrics(username, request, startTime, authenticatorType, SUCCESS_STATUS, null, Fido2MetricType.FIDO2_REGISTRATION_SUCCESS); } /** @@ -195,7 +196,7 @@ public void recordPasskeyAuthenticationAttempt(String username, HttpServletReque * @param authenticatorType Type of authenticator used */ public void recordPasskeyAuthenticationSuccess(String username, HttpServletRequest request, long startTime, String authenticatorType) { - recordAuthenticationMetrics(username, request, startTime, authenticatorType, "SUCCESS", null, Fido2MetricType.FIDO2_AUTHENTICATION_SUCCESS); + recordAuthenticationMetrics(username, request, startTime, authenticatorType, SUCCESS_STATUS, null, Fido2MetricType.FIDO2_AUTHENTICATION_SUCCESS); } /** @@ -576,7 +577,7 @@ private void updateUserMetrics(Fido2MetricsData metricsData) { UserMetricsUpdateRequest request = new UserMetricsUpdateRequest(); request.setUserId(metricsData.getUserId()); request.setUsername(metricsData.getUsername()); - request.setSuccess("SUCCESS".equals(metricsData.getOperationStatus())); + request.setSuccess(SUCCESS_STATUS.equals(metricsData.getOperationStatus())); request.setAuthenticatorType(metricsData.getAuthenticatorType()); request.setDurationMs(metricsData.getDurationMs()); diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/ws/rs/controller/Fido2MetricsController.java b/jans-fido2/server/src/main/java/io/jans/fido2/ws/rs/controller/Fido2MetricsController.java index f8499298944..f69acf5a423 100644 --- a/jans-fido2/server/src/main/java/io/jans/fido2/ws/rs/controller/Fido2MetricsController.java +++ b/jans-fido2/server/src/main/java/io/jans/fido2/ws/rs/controller/Fido2MetricsController.java @@ -72,8 +72,8 @@ public Response getMetricsEntries( return processRequest(() -> { checkMetricsEnabled(); - LocalDateTime start = parseDateTime(startTime, "startTime"); - LocalDateTime end = parseDateTime(endTime, "endTime"); + LocalDateTime start = parseDateTime(startTime, Fido2MetricsConstants.PARAM_START_TIME); + LocalDateTime end = parseDateTime(endTime, Fido2MetricsConstants.PARAM_END_TIME); List entries = metricsService.getMetricsEntries(start, end); return Response.ok(dataMapperService.writeValueAsString(entries)).build(); @@ -102,8 +102,8 @@ public Response getMetricsEntriesByUser( throw errorResponseFactory.invalidRequest("userId is required"); } - LocalDateTime start = parseDateTime(startTime, "startTime"); - LocalDateTime end = parseDateTime(endTime, "endTime"); + LocalDateTime start = parseDateTime(startTime, Fido2MetricsConstants.PARAM_START_TIME); + LocalDateTime end = parseDateTime(endTime, Fido2MetricsConstants.PARAM_END_TIME); List entries = metricsService.getMetricsEntriesByUser(userId, start, end); return Response.ok(dataMapperService.writeValueAsString(entries)).build(); @@ -132,8 +132,8 @@ public Response getMetricsEntriesByOperation( throw errorResponseFactory.invalidRequest("operationType is required"); } - LocalDateTime start = parseDateTime(startTime, "startTime"); - LocalDateTime end = parseDateTime(endTime, "endTime"); + LocalDateTime start = parseDateTime(startTime, Fido2MetricsConstants.PARAM_START_TIME); + LocalDateTime end = parseDateTime(endTime, Fido2MetricsConstants.PARAM_END_TIME); List entries = metricsService.getMetricsEntriesByOperation(operationType, start, end); return Response.ok(dataMapperService.writeValueAsString(entries)).build(); @@ -160,8 +160,8 @@ public Response getAggregations( validateAggregationType(aggregationType); - LocalDateTime start = parseDateTime(startTime, "startTime"); - LocalDateTime end = parseDateTime(endTime, "endTime"); + LocalDateTime start = parseDateTime(startTime, Fido2MetricsConstants.PARAM_START_TIME); + LocalDateTime end = parseDateTime(endTime, Fido2MetricsConstants.PARAM_END_TIME); List aggregations = metricsService.getAggregations(aggregationType, start, end); return Response.ok(dataMapperService.writeValueAsString(aggregations)).build(); @@ -188,8 +188,8 @@ public Response getAggregationSummary( validateAggregationType(aggregationType); - LocalDateTime start = parseDateTime(startTime, "startTime"); - LocalDateTime end = parseDateTime(endTime, "endTime"); + LocalDateTime start = parseDateTime(startTime, Fido2MetricsConstants.PARAM_START_TIME); + LocalDateTime end = parseDateTime(endTime, Fido2MetricsConstants.PARAM_END_TIME); Map summary = metricsService.getAggregationSummary(aggregationType, start, end); return Response.ok(dataMapperService.writeValueAsString(summary)).build(); @@ -212,8 +212,8 @@ public Response getUserAdoptionMetrics( return processRequest(() -> { checkMetricsEnabled(); - LocalDateTime start = parseDateTime(startTime, "startTime"); - LocalDateTime end = parseDateTime(endTime, "endTime"); + LocalDateTime start = parseDateTime(startTime, Fido2MetricsConstants.PARAM_START_TIME); + LocalDateTime end = parseDateTime(endTime, Fido2MetricsConstants.PARAM_END_TIME); Map adoption = metricsService.getUserAdoptionMetrics(start, end); return Response.ok(dataMapperService.writeValueAsString(adoption)).build(); @@ -236,8 +236,8 @@ public Response getPerformanceMetrics( return processRequest(() -> { checkMetricsEnabled(); - LocalDateTime start = parseDateTime(startTime, "startTime"); - LocalDateTime end = parseDateTime(endTime, "endTime"); + LocalDateTime start = parseDateTime(startTime, Fido2MetricsConstants.PARAM_START_TIME); + LocalDateTime end = parseDateTime(endTime, Fido2MetricsConstants.PARAM_END_TIME); Map performance = metricsService.getPerformanceMetrics(start, end); return Response.ok(dataMapperService.writeValueAsString(performance)).build(); @@ -260,8 +260,8 @@ public Response getDeviceAnalytics( return processRequest(() -> { checkMetricsEnabled(); - LocalDateTime start = parseDateTime(startTime, "startTime"); - LocalDateTime end = parseDateTime(endTime, "endTime"); + LocalDateTime start = parseDateTime(startTime, Fido2MetricsConstants.PARAM_START_TIME); + LocalDateTime end = parseDateTime(endTime, Fido2MetricsConstants.PARAM_END_TIME); Map devices = metricsService.getDeviceAnalytics(start, end); return Response.ok(dataMapperService.writeValueAsString(devices)).build(); @@ -284,8 +284,8 @@ public Response getErrorAnalysis( return processRequest(() -> { checkMetricsEnabled(); - LocalDateTime start = parseDateTime(startTime, "startTime"); - LocalDateTime end = parseDateTime(endTime, "endTime"); + LocalDateTime start = parseDateTime(startTime, Fido2MetricsConstants.PARAM_START_TIME); + LocalDateTime end = parseDateTime(endTime, Fido2MetricsConstants.PARAM_END_TIME); Map errors = metricsService.getErrorAnalysis(start, end); return Response.ok(dataMapperService.writeValueAsString(errors)).build(); @@ -312,8 +312,8 @@ public Response getTrendAnalysis( validateAggregationType(aggregationType); - LocalDateTime start = parseDateTime(startTime, "startTime"); - LocalDateTime end = parseDateTime(endTime, "endTime"); + LocalDateTime start = parseDateTime(startTime, Fido2MetricsConstants.PARAM_START_TIME); + LocalDateTime end = parseDateTime(endTime, Fido2MetricsConstants.PARAM_END_TIME); Map trends = metricsService.getTrendAnalysis(aggregationType, start, end); return Response.ok(dataMapperService.writeValueAsString(trends)).build(); @@ -465,10 +465,14 @@ private Response processRequest(RequestProcessor processor) { try { return processor.process(); } catch (WebApplicationException e) { + // Re-throw web application exceptions as-is (they already have proper status codes) throw e; } catch (Exception e) { + // Log the full exception with stack trace for debugging log.error("Error processing metrics request: {}", e.getMessage(), e); - throw errorResponseFactory.unknownError(e.getMessage()); + // Wrap in a proper error response with context + String errorMessage = "An unexpected error occurred while processing the metrics request: " + e.getMessage(); + throw errorResponseFactory.unknownError(errorMessage); } }