Add Data Access Layer with repository interfaces and Table Storage implementations

Introduces a clean abstraction layer to decouple domain services from Azure Table Storage,
enabling future migration to Cosmos DB and Event Streaming without touching business logic.

Architecture:
- 10 repository interfaces in Shared/DataAccess/ (ISessionRepository, IRuleRepository, etc.)
- IDataEventPublisher interface with NullDataEventPublisher for future event streaming
- IStorageInitializer for provider-agnostic initialization
- 5 Table Storage implementations that delegate to existing TableStorageService
- DI extension method AddTableStorageDataAccess() with AddEventStreaming<T>() hook
- Migration plan in tasks/todo.md for incremental adoption

https://claude.ai/code/session_01NHWJfPaa8TQHuPzMD7gG63
This commit is contained in:
Claude
2026-03-27 06:03:55 +00:00
parent ce1833231c
commit 36554c5ca4
21 changed files with 904 additions and 14 deletions
@@ -0,0 +1,58 @@
using AutopilotMonitor.Shared.DataAccess;
using AutopilotMonitor.Functions.DataAccess.TableStorage;
using Microsoft.Extensions.DependencyInjection;
namespace AutopilotMonitor.Functions.DataAccess
{
/// <summary>
/// Extension methods for registering Data Access Layer services.
/// Currently registers Azure Table Storage implementations.
/// To switch to Cosmos DB, replace the implementation registrations here.
/// </summary>
public static class DataAccessServiceExtensions
{
/// <summary>
/// Registers all Data Access Layer services with Table Storage implementations.
/// </summary>
public static IServiceCollection AddTableStorageDataAccess(this IServiceCollection services)
{
// Event publisher (no-op by default; replace with Event Hub/Service Bus implementation)
services.AddSingleton<IDataEventPublisher, NullDataEventPublisher>();
// Storage initializer
services.AddSingleton<IStorageInitializer, TableStorageInitializer>();
// Repository implementations (delegate to existing TableStorageService)
services.AddSingleton<ISessionRepository, TableSessionRepository>();
services.AddSingleton<IRuleRepository, TableRuleRepository>();
services.AddSingleton<IMetricsRepository, TableMetricsRepository>();
services.AddSingleton<IMaintenanceRepository, TableMaintenanceRepository>();
services.AddSingleton<IVulnerabilityRepository, TableVulnerabilityRepository>();
// Note: IConfigRepository, IAdminRepository, IDeviceSecurityRepository,
// IBootstrapRepository, and INotificationRepository implementations will be
// added as existing services are migrated to use the DAL interfaces.
// Currently those services (TenantConfigurationService, GlobalAdminService, etc.)
// manage their own Table Storage connections directly.
return services;
}
/// <summary>
/// Replaces the default NullDataEventPublisher with a custom implementation.
/// Call this AFTER AddTableStorageDataAccess() to override.
/// Example: services.AddEventStreaming&lt;EventHubPublisher&gt;();
/// </summary>
public static IServiceCollection AddEventStreaming<TPublisher>(this IServiceCollection services)
where TPublisher : class, IDataEventPublisher
{
// Remove existing registration and replace
var descriptor = services.FirstOrDefault(d => d.ServiceType == typeof(IDataEventPublisher));
if (descriptor != null)
services.Remove(descriptor);
services.AddSingleton<IDataEventPublisher, TPublisher>();
return services;
}
}
}
@@ -0,0 +1,63 @@
using AutopilotMonitor.Shared.DataAccess;
using AutopilotMonitor.Shared.Models;
using AutopilotMonitor.Functions.Services;
namespace AutopilotMonitor.Functions.DataAccess.TableStorage
{
/// <summary>
/// Table Storage implementation of IMaintenanceRepository.
/// Delegates to existing TableStorageService for backwards compatibility.
/// </summary>
public class TableMaintenanceRepository : IMaintenanceRepository
{
private readonly TableStorageService _storage;
public TableMaintenanceRepository(TableStorageService storage)
{
_storage = storage;
}
public Task<bool> LogAuditEntryAsync(string tenantId, string action, string entityType,
string entityId, string performedBy, Dictionary<string, string>? details = null)
=> _storage.LogAuditEntryAsync(tenantId, action, entityType, entityId, performedBy, details);
public Task<List<AuditLogEntry>> GetAuditLogsAsync(string tenantId, int maxResults = 100)
=> _storage.GetAuditLogsAsync(tenantId, maxResults);
public Task<List<AuditLogEntry>> GetAllAuditLogsAsync(int maxResults = 100)
=> _storage.GetAllAuditLogsAsync(maxResults);
public Task<List<SessionSummary>> GetSessionsOlderThanAsync(string tenantId, DateTime cutoffDate)
=> _storage.GetSessionsOlderThanAsync(tenantId, cutoffDate);
public Task<List<SessionSummary>> GetSessionsByDateRangeAsync(DateTime startDate, DateTime endDate, string? tenantId = null)
=> _storage.GetSessionsByDateRangeAsync(startDate, endDate, tenantId);
public Task<List<SessionSummary>> GetStalledSessionsAsync(string tenantId, DateTime cutoffTime)
=> _storage.GetStalledSessionsAsync(tenantId, cutoffTime);
public Task<List<SessionSummary>> GetExcessiveDataSendersAsync(string tenantId, DateTime windowCutoff, int maxSessionWindowHours)
=> _storage.GetExcessiveDataSendersAsync(tenantId, windowCutoff, maxSessionWindowHours);
public Task<List<string>> GetAllTenantIdsAsync()
=> _storage.GetAllTenantIdsAsync();
public Task<int> DeleteSessionEventsAsync(string tenantId, string sessionId)
=> _storage.DeleteSessionEventsAsync(tenantId, sessionId);
public Task<int> DeleteSessionRuleResultsAsync(string tenantId, string sessionId)
=> _storage.DeleteSessionRuleResultsAsync(tenantId, sessionId);
public Task<int> DeleteSessionAppInstallSummariesAsync(string tenantId, string sessionId)
=> _storage.DeleteSessionAppInstallSummariesAsync(tenantId, sessionId);
public Task<int> BackfillSessionIndexAsync()
=> _storage.BackfillSessionIndexAsync();
public Task<int> CleanupGhostSessionIndexEntriesAsync()
=> _storage.CleanupGhostSessionIndexEntriesAsync();
public Task<bool> IsSessionIndexEmptyAsync()
=> _storage.IsSessionIndexEmptyAsync();
}
}
@@ -0,0 +1,65 @@
using AutopilotMonitor.Shared.DataAccess;
using AutopilotMonitor.Shared.Models;
using AutopilotMonitor.Functions.Services;
namespace AutopilotMonitor.Functions.DataAccess.TableStorage
{
/// <summary>
/// Table Storage implementation of IMetricsRepository.
/// Delegates to existing TableStorageService for backwards compatibility.
/// </summary>
public class TableMetricsRepository : IMetricsRepository
{
private readonly TableStorageService _storage;
private readonly IDataEventPublisher _publisher;
public TableMetricsRepository(TableStorageService storage, IDataEventPublisher publisher)
{
_storage = storage;
_publisher = publisher;
}
public Task<bool> SaveUsageMetricsSnapshotAsync(UsageMetricsSnapshot metrics)
=> _storage.SaveUsageMetricsSnapshotAsync(metrics);
public Task<List<UsageMetricsSnapshot>> GetUsageMetricsSnapshotAsync(
string? tenantId = null, string? startDate = null, string? endDate = null, int maxResults = 100)
=> _storage.GetUsageMetricsSnapshotAsync(tenantId, startDate, endDate, maxResults);
public Task<bool> HasUsageMetricsSnapshotAsync(string date)
=> _storage.HasUsageMetricsSnapshotAsync(date);
public Task<bool> StoreAppInstallSummaryAsync(AppInstallSummary summary)
=> _storage.StoreAppInstallSummaryAsync(summary);
public Task<List<AppInstallSummary>> GetAppInstallSummariesByTenantAsync(string tenantId)
=> _storage.GetAppInstallSummariesByTenantAsync(tenantId);
public Task<List<AppInstallSummary>> GetAllAppInstallSummariesAsync()
=> _storage.GetAllAppInstallSummariesAsync();
public Task<PlatformStats?> GetPlatformStatsAsync()
=> _storage.GetPlatformStatsAsync();
public Task<bool> SavePlatformStatsAsync(PlatformStats stats)
=> _storage.SavePlatformStatsAsync(stats);
public Task IncrementPlatformStatAsync(string field, long amount = 1)
=> _storage.IncrementPlatformStatAsync(field, amount);
public Task RecordUserLoginAsync(string tenantId, string upn, string? displayName, string? objectId)
=> _storage.RecordUserLoginAsync(tenantId, upn, displayName, objectId);
public Task<UserActivityMetrics> GetUserActivityMetricsAsync(string tenantId)
=> _storage.GetUserActivityMetricsAsync(tenantId);
public Task<UserActivityMetrics> GetAllUserActivityMetricsAsync()
=> _storage.GetAllUserActivityMetricsAsync();
public Task<(int uniqueUsers, int loginCount)> GetUserActivityForDateAsync(string? tenantId, DateTime date)
=> _storage.GetUserActivityForDateAsync(tenantId, date);
public Task<List<object>> GetMetricsSummaryAsync(string? tenantId)
=> _storage.GetMetricsSummaryAsync(tenantId);
}
}
@@ -0,0 +1,69 @@
using AutopilotMonitor.Shared.DataAccess;
using AutopilotMonitor.Shared.Models;
using AutopilotMonitor.Functions.Services;
namespace AutopilotMonitor.Functions.DataAccess.TableStorage
{
/// <summary>
/// Table Storage implementation of IRuleRepository.
/// Delegates to existing TableStorageService for backwards compatibility.
/// </summary>
public class TableRuleRepository : IRuleRepository
{
private readonly TableStorageService _storage;
private readonly IDataEventPublisher _publisher;
public TableRuleRepository(TableStorageService storage, IDataEventPublisher publisher)
{
_storage = storage;
_publisher = publisher;
}
public async Task<bool> StoreRuleResultAsync(RuleResult result)
{
var success = await _storage.StoreRuleResultAsync(result);
if (success)
await _publisher.PublishAsync("rule.evaluated", result, result.TenantId);
return success;
}
public Task<List<RuleResult>> GetRuleResultsAsync(string tenantId, string sessionId)
=> _storage.GetRuleResultsAsync(tenantId, sessionId);
public Task<bool> StoreGatherRuleAsync(GatherRule rule, string tenantId = "global")
=> _storage.StoreGatherRuleAsync(rule, tenantId);
public Task<List<GatherRule>> GetGatherRulesAsync(string partitionKey)
=> _storage.GetGatherRulesAsync(partitionKey);
public Task<bool> DeleteGatherRuleAsync(string tenantId, string ruleId)
=> _storage.DeleteGatherRuleAsync(tenantId, ruleId);
public Task<bool> StoreRuleStateAsync(string tenantId, string ruleId, bool enabled)
=> _storage.StoreRuleStateAsync(tenantId, ruleId, enabled);
public Task<Dictionary<string, bool>> GetRuleStatesAsync(string tenantId)
=> _storage.GetRuleStatesAsync(tenantId);
public Task<bool> DeleteRuleStateAsync(string tenantId, string ruleId)
=> _storage.DeleteRuleStateAsync(tenantId, ruleId);
public Task<bool> StoreAnalyzeRuleAsync(AnalyzeRule rule, string tenantId = "global")
=> _storage.StoreAnalyzeRuleAsync(rule, tenantId);
public Task<List<AnalyzeRule>> GetAnalyzeRulesAsync(string partitionKey)
=> _storage.GetAnalyzeRulesAsync(partitionKey);
public Task<bool> DeleteAnalyzeRuleAsync(string tenantId, string ruleId)
=> _storage.DeleteAnalyzeRuleAsync(tenantId, ruleId);
public Task<bool> StoreImeLogPatternAsync(ImeLogPattern pattern, string tenantId = "global")
=> _storage.StoreImeLogPatternAsync(pattern, tenantId);
public Task<List<ImeLogPattern>> GetImeLogPatternsAsync(string partitionKey)
=> _storage.GetImeLogPatternsAsync(partitionKey);
public Task<bool> DeleteImeLogPatternAsync(string tenantId, string patternId)
=> _storage.DeleteImeLogPatternAsync(tenantId, patternId);
}
}
@@ -0,0 +1,102 @@
using AutopilotMonitor.Shared.DataAccess;
using AutopilotMonitor.Shared.Models;
using AutopilotMonitor.Functions.Services;
namespace AutopilotMonitor.Functions.DataAccess.TableStorage
{
/// <summary>
/// Table Storage implementation of ISessionRepository.
/// Delegates to existing TableStorageService for backwards compatibility.
/// </summary>
public class TableSessionRepository : ISessionRepository
{
private readonly TableStorageService _storage;
private readonly IDataEventPublisher _publisher;
public TableSessionRepository(TableStorageService storage, IDataEventPublisher publisher)
{
_storage = storage;
_publisher = publisher;
}
public async Task<bool> StoreSessionAsync(SessionRegistration registration)
{
var result = await _storage.StoreSessionAsync(registration);
if (result)
await _publisher.PublishAsync("session.created", registration, registration.TenantId);
return result;
}
public Task<SessionSummary?> GetSessionAsync(string tenantId, string sessionId)
=> _storage.GetSessionAsync(tenantId, sessionId);
public Task<SessionPage> GetSessionsAsync(string tenantId, int maxResults = 100, string? cursor = null)
=> _storage.GetSessionsAsync(tenantId, maxResults, cursor);
public Task<SessionPage> GetAllSessionsAsync(int maxResults = 100, string? cursor = null)
=> _storage.GetAllSessionsAsync(maxResults, cursor);
public Task<bool> DeleteSessionAsync(string tenantId, string sessionId)
=> _storage.DeleteSessionAsync(tenantId, sessionId);
public Task<bool> UpdateSessionStatusAsync(
string tenantId, string sessionId, SessionStatus status,
EnrollmentPhase? currentPhase = null, string? failureReason = null,
DateTime? completedAt = null, DateTime? earliestEventTimestamp = null,
DateTime? latestEventTimestamp = null, bool? isPreProvisioned = null,
bool? isUserDriven = null, DateTime? resumedAt = null)
=> _storage.UpdateSessionStatusAsync(tenantId, sessionId, status,
currentPhase, failureReason, completedAt, earliestEventTimestamp,
latestEventTimestamp, isPreProvisioned, isUserDriven, resumedAt);
public Task IncrementSessionEventCountAsync(
string tenantId, string sessionId, int increment,
DateTime? earliestEventTimestamp = null, DateTime? latestEventTimestamp = null,
EnrollmentPhase? currentPhase = null)
=> _storage.IncrementSessionEventCountAsync(tenantId, sessionId, increment,
earliestEventTimestamp, latestEventTimestamp, currentPhase);
public Task UpdateSessionDiagnosticsBlobAsync(string tenantId, string sessionId, string blobName)
=> _storage.UpdateSessionDiagnosticsBlobAsync(tenantId, sessionId, blobName);
public Task SetSessionPreProvisionedAsync(string tenantId, string sessionId, bool isPreProvisioned,
SessionStatus? status = null, bool? isUserDriven = null)
=> _storage.SetSessionPreProvisionedAsync(tenantId, sessionId, isPreProvisioned, status, isUserDriven);
public Task UpdateSessionGeoAsync(string tenantId, string sessionId,
string? country, string? region, string? city, string? loc)
=> _storage.UpdateSessionGeoAsync(tenantId, sessionId, country, region, city, loc);
public Task UpdateSessionImeAgentVersionAsync(string tenantId, string sessionId, string version)
=> _storage.UpdateSessionImeAgentVersionAsync(tenantId, sessionId, version);
public async Task<bool> StoreEventAsync(EnrollmentEvent evt)
{
var result = await _storage.StoreEventAsync(evt);
if (result)
await _publisher.PublishAsync("event.ingested", evt, evt.TenantId);
return result;
}
public Task<List<SessionSummary>> SearchSessionsAsync(string? tenantId, SessionSearchFilter filter)
=> _storage.SearchSessionsAsync(tenantId, filter);
public Task<List<SessionSummary>> SearchSessionsByEventAsync(
string? tenantId, string eventType, string? source, string? severity,
string? phase, int limit = 50)
=> _storage.SearchSessionsByEventAsync(tenantId, eventType, source, severity, phase, limit);
public Task<List<SessionSummary>> SearchSessionsByCveAsync(
string? tenantId, string cveId, double? minCvssScore, string? overallRisk, int limit = 50)
=> _storage.SearchSessionsByCveAsync(tenantId, cveId, minCvssScore, overallRisk, limit);
public Task UpsertEventTypeIndexBatchAsync(string tenantId, string sessionId, IEnumerable<string> eventTypes)
=> _storage.UpsertEventTypeIndexBatchAsync(tenantId, sessionId, eventTypes);
public Task UpsertDeviceSnapshotAsync(string tenantId, string sessionId, IEnumerable<EnrollmentEvent> events)
=> _storage.UpsertDeviceSnapshotAsync(tenantId, sessionId, events);
public Task UpsertCveIndexEntriesAsync(string tenantId, string sessionId, List<Dictionary<string, object>> findings)
=> _storage.UpsertCveIndexEntriesAsync(tenantId, sessionId, findings);
}
}
@@ -0,0 +1,22 @@
using AutopilotMonitor.Shared.DataAccess;
using AutopilotMonitor.Functions.Services;
namespace AutopilotMonitor.Functions.DataAccess.TableStorage
{
/// <summary>
/// Table Storage implementation of IStorageInitializer.
/// Delegates to existing TableStorageService for backwards compatibility.
/// </summary>
public class TableStorageInitializer : IStorageInitializer
{
private readonly TableStorageService _storage;
public TableStorageInitializer(TableStorageService storage)
{
_storage = storage;
}
public Task InitializeAsync()
=> _storage.InitializeTablesAsync();
}
}
@@ -0,0 +1,43 @@
using AutopilotMonitor.Shared.DataAccess;
using AutopilotMonitor.Functions.Services;
namespace AutopilotMonitor.Functions.DataAccess.TableStorage
{
/// <summary>
/// Table Storage implementation of IVulnerabilityRepository.
/// Delegates to existing TableStorageService for backwards compatibility.
/// </summary>
public class TableVulnerabilityRepository : IVulnerabilityRepository
{
private readonly TableStorageService _storage;
private readonly IDataEventPublisher _publisher;
public TableVulnerabilityRepository(TableStorageService storage, IDataEventPublisher publisher)
{
_storage = storage;
_publisher = publisher;
}
public async Task StoreVulnerabilityReportAsync(string tenantId, string sessionId, Dictionary<string, object> reportData)
{
await _storage.StoreVulnerabilityReportAsync(tenantId, sessionId, reportData);
await _publisher.PublishAsync("vulnerability.reported", new { tenantId, sessionId, reportData }, tenantId);
}
public Task<Dictionary<string, object>?> GetVulnerabilityReportAsync(string tenantId, string sessionId)
=> _storage.GetVulnerabilityReportAsync(tenantId, sessionId);
public Task UpsertSoftwareInventoryAsync(string tenantId, List<Dictionary<string, object>> inventoryItems,
string sessionId, Dictionary<string, string?>? cpeMappings = null)
=> _storage.UpsertSoftwareInventoryAsync(tenantId, inventoryItems, sessionId, cpeMappings);
public Task<List<Dictionary<string, object>>> GetSoftwareInventoryAsync(string tenantId)
=> _storage.GetSoftwareInventoryAsync(tenantId);
public Task<int> ImportCpeMappingSeedFromJsonAsync(string json)
=> _storage.ImportCpeMappingSeedFromJsonAsync(json);
public Task<int> ImportCpeCommunityMappingsFromJsonAsync(string json)
=> _storage.ImportCpeCommunityMappingsFromJsonAsync(json);
}
}
@@ -1,5 +1,6 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using AutopilotMonitor.Functions.DataAccess;
using AutopilotMonitor.Functions.Functions.Config;
using AutopilotMonitor.Functions.Functions.Ingest;
using AutopilotMonitor.Functions.Functions.Sessions;
@@ -100,6 +101,11 @@ builder.Services.AddMemoryCache();
builder.Services.AddHttpClient();
builder.Services.AddSingleton<TableStorageService>();
builder.Services.AddHostedService<TableInitializerService>(); // Initialize all tables at startup
// Data Access Layer — repository interfaces backed by Table Storage.
// To switch to Cosmos DB: replace AddTableStorageDataAccess() with AddCosmosDataAccess().
// To add event streaming: chain .AddEventStreaming<EventHubPublisher>() after this call.
builder.Services.AddTableStorageDataAccess();
builder.Services.AddSingleton<TenantConfigurationService>();
builder.Services.AddSingleton<AdminConfigurationService>();
builder.Services.AddSingleton<RateLimitService>();
@@ -0,0 +1,64 @@
namespace AutopilotMonitor.Shared.DataAccess
{
/// <summary>
/// Repository for admin and tenant member management.
/// Covers: GlobalAdmins, TenantAdmins, ApiKeys tables.
/// </summary>
public interface IAdminRepository
{
// --- Global Admins ---
Task<bool> IsGlobalAdminAsync(string upn);
Task<List<GlobalAdminEntry>> GetAllGlobalAdminsAsync();
Task<bool> AddGlobalAdminAsync(string upn, string addedBy);
Task<bool> RemoveGlobalAdminAsync(string upn);
Task<bool> DisableGlobalAdminAsync(string upn);
// --- Tenant Members ---
Task<List<TenantMember>> GetTenantMembersAsync(string tenantId);
Task<bool> AddTenantMemberAsync(string tenantId, string upn, string addedBy, string role, bool canManageBootstrapTokens = false);
Task<bool> RemoveTenantMemberAsync(string tenantId, string upn);
Task<bool> UpdateMemberPermissionsAsync(string tenantId, string upn, string role, bool canManageBootstrapTokens);
Task<TenantMember?> GetTenantMemberAsync(string tenantId, string upn);
Task<bool> IsTenantAdminAsync(string tenantId, string upn);
Task<bool> IsTenantMemberAsync(string tenantId, string upn);
// --- API Keys ---
Task<ApiKeyEntry?> ValidateApiKeyAsync(string apiKeyHash);
Task<bool> StoreApiKeyAsync(string tenantId, ApiKeyEntry entry);
Task<List<ApiKeyEntry>> GetApiKeysAsync(string tenantId);
Task<bool> RevokeApiKeyAsync(string tenantId, string keyId);
}
public class GlobalAdminEntry
{
public string Upn { get; set; } = string.Empty;
public bool IsEnabled { get; set; } = true;
public DateTime AddedAt { get; set; }
public string AddedBy { get; set; } = string.Empty;
}
public class TenantMember
{
public string Upn { get; set; } = string.Empty;
public string TenantId { get; set; } = string.Empty;
public string Role { get; set; } = "Admin";
public bool IsEnabled { get; set; } = true;
public bool CanManageBootstrapTokens { get; set; }
public DateTime AddedAt { get; set; }
public string AddedBy { get; set; } = string.Empty;
}
public class ApiKeyEntry
{
public string KeyId { get; set; } = string.Empty;
public string TenantId { get; set; } = string.Empty;
public string KeyHash { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Scope { get; set; } = "tenant";
public string CreatedBy { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public DateTime? ExpiresAt { get; set; }
public bool IsActive { get; set; } = true;
public long RequestCount { get; set; }
}
}
@@ -0,0 +1,18 @@
using AutopilotMonitor.Shared.Models;
namespace AutopilotMonitor.Shared.DataAccess
{
/// <summary>
/// Repository for bootstrap session management.
/// Covers: BootstrapSessions table.
/// </summary>
public interface IBootstrapRepository
{
Task<bool> CreateBootstrapSessionAsync(BootstrapSession session);
Task<BootstrapSession?> GetBootstrapSessionByCodeAsync(string shortCode);
Task<BootstrapSession?> ValidateBootstrapTokenAsync(string token);
Task<List<BootstrapSession>> GetBootstrapSessionsAsync(string tenantId);
Task<bool> RevokeBootstrapSessionAsync(string shortCode);
Task<bool> IncrementBootstrapUsageAsync(string shortCode);
}
}
@@ -0,0 +1,30 @@
using AutopilotMonitor.Shared.Models;
namespace AutopilotMonitor.Shared.DataAccess
{
/// <summary>
/// Repository for tenant and admin configuration.
/// Covers: TenantConfiguration, AdminConfiguration, PreviewWhitelist, PreviewConfig tables.
/// </summary>
public interface IConfigRepository
{
// --- Tenant Configuration ---
Task<TenantConfiguration?> GetTenantConfigurationAsync(string tenantId);
Task<bool> SaveTenantConfigurationAsync(TenantConfiguration config);
Task<List<TenantConfiguration>> GetAllTenantConfigurationsAsync();
// --- Admin Configuration ---
Task<AdminConfiguration?> GetAdminConfigurationAsync();
Task<bool> SaveAdminConfigurationAsync(AdminConfiguration config);
// --- Preview Whitelist ---
Task<bool> IsInPreviewWhitelistAsync(string tenantId);
Task<bool> AddToPreviewWhitelistAsync(string tenantId, string addedBy);
Task<bool> RemoveFromPreviewWhitelistAsync(string tenantId);
Task<List<string>> GetPreviewWhitelistAsync();
// --- Preview Config ---
Task<Dictionary<string, string>> GetPreviewConfigAsync();
Task<bool> SavePreviewConfigAsync(string key, string value);
}
}
@@ -0,0 +1,27 @@
namespace AutopilotMonitor.Shared.DataAccess
{
/// <summary>
/// Publishes data change events for downstream consumers (Event Hub, Service Bus, etc.).
/// Default implementation is NullDataEventPublisher (no-op).
/// </summary>
public interface IDataEventPublisher
{
/// <summary>
/// Publishes a data change event.
/// </summary>
/// <param name="eventType">Domain event type (e.g. "session.created", "event.ingested", "rule.evaluated")</param>
/// <param name="payload">The event payload (typically the domain object)</param>
/// <param name="tenantId">Optional tenant context for routing</param>
Task PublishAsync(string eventType, object payload, string? tenantId = null);
}
/// <summary>
/// No-op implementation of IDataEventPublisher.
/// Used when event streaming is not configured.
/// </summary>
public class NullDataEventPublisher : IDataEventPublisher
{
public Task PublishAsync(string eventType, object payload, string? tenantId = null)
=> Task.CompletedTask;
}
}
@@ -0,0 +1,44 @@
namespace AutopilotMonitor.Shared.DataAccess
{
/// <summary>
/// Repository for device and version blocking.
/// Covers: BlockedDevices, BlockedVersions tables.
/// </summary>
public interface IDeviceSecurityRepository
{
// --- Blocked Devices ---
Task<(bool isBlocked, DateTime? unblockAt, string action)> IsDeviceBlockedAsync(string tenantId, string serialNumber);
Task<List<BlockedDeviceEntry>> GetBlockedDevicesAsync(string tenantId);
Task<List<BlockedDeviceEntry>> GetAllBlockedDevicesAsync();
Task BlockDeviceAsync(string tenantId, string serialNumber, int durationHours,
string blockedByEmail, string? reason = null, string action = "Block");
Task UnblockDeviceAsync(string tenantId, string serialNumber);
// --- Blocked Versions ---
Task<(bool isBlocked, string action, string? matchedPattern)> IsVersionBlockedAsync(string agentVersion);
Task<List<BlockedVersionEntry>> GetBlockedVersionsAsync();
Task BlockVersionAsync(string versionPattern, string action, string createdByEmail, string? reason = null);
Task UnblockVersionAsync(string versionPattern);
}
public class BlockedDeviceEntry
{
public string TenantId { get; set; } = string.Empty;
public string SerialNumber { get; set; } = string.Empty;
public DateTime BlockedAt { get; set; }
public DateTime? UnblockAt { get; set; }
public string? BlockedByEmail { get; set; }
public int DurationHours { get; set; }
public string? Reason { get; set; }
public string Action { get; set; } = "Block";
}
public class BlockedVersionEntry
{
public string VersionPattern { get; set; } = string.Empty;
public string Action { get; set; } = "Block";
public string CreatedByEmail { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public string? Reason { get; set; }
}
}
@@ -0,0 +1,36 @@
using AutopilotMonitor.Shared.Models;
namespace AutopilotMonitor.Shared.DataAccess
{
/// <summary>
/// Repository for audit logs and data retention/maintenance operations.
/// Covers: AuditLogs table + maintenance queries across Sessions/Events/RuleResults.
/// </summary>
public interface IMaintenanceRepository
{
// --- Audit Logs ---
Task<bool> LogAuditEntryAsync(string tenantId, string action, string entityType,
string entityId, string performedBy, Dictionary<string, string>? details = null);
Task<List<AuditLogEntry>> GetAuditLogsAsync(string tenantId, int maxResults = 100);
Task<List<AuditLogEntry>> GetAllAuditLogsAsync(int maxResults = 100);
// --- Data Retention Queries ---
Task<List<SessionSummary>> GetSessionsOlderThanAsync(string tenantId, DateTime cutoffDate);
Task<List<SessionSummary>> GetSessionsByDateRangeAsync(DateTime startDate, DateTime endDate, string? tenantId = null);
Task<List<SessionSummary>> GetStalledSessionsAsync(string tenantId, DateTime cutoffTime);
Task<List<SessionSummary>> GetExcessiveDataSendersAsync(string tenantId, DateTime windowCutoff, int maxSessionWindowHours);
// --- Tenant Discovery ---
Task<List<string>> GetAllTenantIdsAsync();
// --- Cleanup ---
Task<int> DeleteSessionEventsAsync(string tenantId, string sessionId);
Task<int> DeleteSessionRuleResultsAsync(string tenantId, string sessionId);
Task<int> DeleteSessionAppInstallSummariesAsync(string tenantId, string sessionId);
// --- Index Maintenance ---
Task<int> BackfillSessionIndexAsync();
Task<int> CleanupGhostSessionIndexEntriesAsync();
Task<bool> IsSessionIndexEmptyAsync();
}
}
@@ -0,0 +1,36 @@
using AutopilotMonitor.Shared.Models;
namespace AutopilotMonitor.Shared.DataAccess
{
/// <summary>
/// Repository for usage metrics, platform stats, and user activity tracking.
/// Covers: UsageMetrics, PlatformStats, UserActivity, AppInstallSummaries tables.
/// </summary>
public interface IMetricsRepository
{
// --- Usage Metrics Snapshots ---
Task<bool> SaveUsageMetricsSnapshotAsync(UsageMetricsSnapshot metrics);
Task<List<UsageMetricsSnapshot>> GetUsageMetricsSnapshotAsync(
string? tenantId = null, string? startDate = null, string? endDate = null, int maxResults = 100);
Task<bool> HasUsageMetricsSnapshotAsync(string date);
// --- App Install Summaries ---
Task<bool> StoreAppInstallSummaryAsync(AppInstallSummary summary);
Task<List<AppInstallSummary>> GetAppInstallSummariesByTenantAsync(string tenantId);
Task<List<AppInstallSummary>> GetAllAppInstallSummariesAsync();
// --- Platform Stats ---
Task<PlatformStats?> GetPlatformStatsAsync();
Task<bool> SavePlatformStatsAsync(PlatformStats stats);
Task IncrementPlatformStatAsync(string field, long amount = 1);
// --- User Activity ---
Task RecordUserLoginAsync(string tenantId, string upn, string? displayName, string? objectId);
Task<UserActivityMetrics> GetUserActivityMetricsAsync(string tenantId);
Task<UserActivityMetrics> GetAllUserActivityMetricsAsync();
Task<(int uniqueUsers, int loginCount)> GetUserActivityForDateAsync(string? tenantId, DateTime date);
// --- Metrics Summary (Agent API) ---
Task<List<object>> GetMetricsSummaryAsync(string? tenantId);
}
}
@@ -0,0 +1,34 @@
using AutopilotMonitor.Shared.Models;
namespace AutopilotMonitor.Shared.DataAccess
{
/// <summary>
/// Repository for global notifications and session reports.
/// Covers: GlobalNotifications, SessionReports tables.
/// </summary>
public interface INotificationRepository
{
// --- Global Notifications ---
Task<bool> AddNotificationAsync(GlobalNotification notification);
Task<List<GlobalNotification>> GetNotificationsAsync(int maxResults = 50);
Task<bool> DismissNotificationAsync(string notificationId, string dismissedBy);
// --- Session Reports ---
Task<bool> StoreSessionReportMetadataAsync(SessionReportMetadata metadata);
Task<List<SessionReportMetadata>> GetSessionReportsAsync(string? tenantId = null, int maxResults = 50);
Task<SessionReportMetadata?> GetSessionReportAsync(string reportId);
}
public class GlobalNotification
{
public string NotificationId { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
public string Severity { get; set; } = "info";
public DateTime CreatedAt { get; set; }
public string CreatedBy { get; set; } = string.Empty;
public bool IsDismissed { get; set; }
public string? DismissedBy { get; set; }
public DateTime? DismissedAt { get; set; }
}
}
@@ -0,0 +1,35 @@
using AutopilotMonitor.Shared.Models;
namespace AutopilotMonitor.Shared.DataAccess
{
/// <summary>
/// Repository for rule definitions, results, and patterns.
/// Covers: RuleResults, GatherRules, AnalyzeRules, ImeLogPatterns, RuleStates tables.
/// </summary>
public interface IRuleRepository
{
// --- Rule Results ---
Task<bool> StoreRuleResultAsync(RuleResult result);
Task<List<RuleResult>> GetRuleResultsAsync(string tenantId, string sessionId);
// --- Gather Rules ---
Task<bool> StoreGatherRuleAsync(GatherRule rule, string tenantId = "global");
Task<List<GatherRule>> GetGatherRulesAsync(string partitionKey);
Task<bool> DeleteGatherRuleAsync(string tenantId, string ruleId);
// --- Rule States ---
Task<bool> StoreRuleStateAsync(string tenantId, string ruleId, bool enabled);
Task<Dictionary<string, bool>> GetRuleStatesAsync(string tenantId);
Task<bool> DeleteRuleStateAsync(string tenantId, string ruleId);
// --- Analyze Rules ---
Task<bool> StoreAnalyzeRuleAsync(AnalyzeRule rule, string tenantId = "global");
Task<List<AnalyzeRule>> GetAnalyzeRulesAsync(string partitionKey);
Task<bool> DeleteAnalyzeRuleAsync(string tenantId, string ruleId);
// --- IME Log Patterns ---
Task<bool> StoreImeLogPatternAsync(ImeLogPattern pattern, string tenantId = "global");
Task<List<ImeLogPattern>> GetImeLogPatternsAsync(string partitionKey);
Task<bool> DeleteImeLogPatternAsync(string tenantId, string patternId);
}
}
@@ -0,0 +1,52 @@
using AutopilotMonitor.Shared.Models;
namespace AutopilotMonitor.Shared.DataAccess
{
/// <summary>
/// Repository for session and event data.
/// Covers: Sessions, SessionsIndex, Events tables.
/// </summary>
public interface ISessionRepository
{
// --- Session CRUD ---
Task<bool> StoreSessionAsync(SessionRegistration registration);
Task<SessionSummary?> GetSessionAsync(string tenantId, string sessionId);
Task<SessionPage> GetSessionsAsync(string tenantId, int maxResults = 100, string? cursor = null);
Task<SessionPage> GetAllSessionsAsync(int maxResults = 100, string? cursor = null);
Task<bool> DeleteSessionAsync(string tenantId, string sessionId);
// --- Session Updates ---
Task<bool> UpdateSessionStatusAsync(
string tenantId, string sessionId, SessionStatus status,
EnrollmentPhase? currentPhase = null, string? failureReason = null,
DateTime? completedAt = null, DateTime? earliestEventTimestamp = null,
DateTime? latestEventTimestamp = null, bool? isPreProvisioned = null,
bool? isUserDriven = null, DateTime? resumedAt = null);
Task IncrementSessionEventCountAsync(
string tenantId, string sessionId, int increment,
DateTime? earliestEventTimestamp = null, DateTime? latestEventTimestamp = null,
EnrollmentPhase? currentPhase = null);
Task UpdateSessionDiagnosticsBlobAsync(string tenantId, string sessionId, string blobName);
Task SetSessionPreProvisionedAsync(string tenantId, string sessionId, bool isPreProvisioned,
SessionStatus? status = null, bool? isUserDriven = null);
Task UpdateSessionGeoAsync(string tenantId, string sessionId,
string? country, string? region, string? city, string? loc);
Task UpdateSessionImeAgentVersionAsync(string tenantId, string sessionId, string version);
// --- Events ---
Task<bool> StoreEventAsync(EnrollmentEvent evt);
// --- Search ---
Task<List<SessionSummary>> SearchSessionsAsync(string? tenantId, SessionSearchFilter filter);
Task<List<SessionSummary>> SearchSessionsByEventAsync(
string? tenantId, string eventType, string? source, string? severity,
string? phase, int limit = 50);
Task<List<SessionSummary>> SearchSessionsByCveAsync(
string? tenantId, string cveId, double? minCvssScore, string? overallRisk, int limit = 50);
// --- Agent Indexes ---
Task UpsertEventTypeIndexBatchAsync(string tenantId, string sessionId, IEnumerable<string> eventTypes);
Task UpsertDeviceSnapshotAsync(string tenantId, string sessionId, IEnumerable<EnrollmentEvent> events);
Task UpsertCveIndexEntriesAsync(string tenantId, string sessionId, List<Dictionary<string, object>> findings);
}
}
@@ -0,0 +1,11 @@
namespace AutopilotMonitor.Shared.DataAccess
{
/// <summary>
/// Initializes the storage backend (creates tables, containers, collections, etc.).
/// Each storage provider implements this for its specific setup needs.
/// </summary>
public interface IStorageInitializer
{
Task InitializeAsync();
}
}
@@ -0,0 +1,17 @@
namespace AutopilotMonitor.Shared.DataAccess
{
/// <summary>
/// Repository for vulnerability data (CPE mappings, CVE reports, software inventory).
/// Covers: VulnerabilityCache, VulnerabilityReports, SoftwareInventory, CveIndex tables.
/// </summary>
public interface IVulnerabilityRepository
{
Task StoreVulnerabilityReportAsync(string tenantId, string sessionId, Dictionary<string, object> reportData);
Task<Dictionary<string, object>?> GetVulnerabilityReportAsync(string tenantId, string sessionId);
Task UpsertSoftwareInventoryAsync(string tenantId, List<Dictionary<string, object>> inventoryItems,
string sessionId, Dictionary<string, string?>? cpeMappings = null);
Task<List<Dictionary<string, object>>> GetSoftwareInventoryAsync(string tenantId);
Task<int> ImportCpeMappingSeedFromJsonAsync(string json);
Task<int> ImportCpeCommunityMappingsFromJsonAsync(string json);
}
}
+72 -14
View File
@@ -1,16 +1,74 @@
# Tenant Config Report (Global Admin)
# Data Access Layer — Migration Plan
## Goal
New global-admin-only page that shows a read-only report of a tenant's full configuration,
including admin-set parameters and computed runtime (agent) parameters.
## Phase 1: Foundation ✅
- [x] Define 10 Repository Interfaces in `Shared/DataAccess/`
- [x] Define `IDataEventPublisher` + `NullDataEventPublisher`
- [x] Define `IStorageInitializer`
- [x] Create 5 core Table Storage implementations (delegate to `TableStorageService`)
- [x] Create `DataAccessServiceExtensions` with `AddTableStorageDataAccess()`
- [x] Register DAL in `Program.cs`
## Plan
- [x] Write plan
- [ ] Create `/app/tenant-config-report/page.tsx` — global admin only, read-only report
- Tenant selector (reuse pattern from gather-rules page)
- Fetch config via `GET /api/config/{tenantId}` (existing endpoint, GA cross-tenant)
- Display all TenantConfiguration fields grouped by section
- Show computed runtime parameters (AgentConfigResponse equivalents with defaults)
- Highlight non-default values for quick scanning
- [ ] Add nav item to global admin section in `globalNavConfig.tsx`
- [ ] Commit and push to feature branch
## Phase 2: Migrate Core Functions to Use Interfaces
These functions currently inject `TableStorageService` directly. Migrate to inject repository interfaces instead.
### Session Functions
- [ ] `GetSessionsFunction` → `ISessionRepository`
- [ ] `GetSessionFunction` → `ISessionRepository`
- [ ] `DeleteSessionFunction` → `ISessionRepository`
- [ ] `RegisterSessionFunction` → `ISessionRepository`
- [ ] `IngestEventsFunction` → `ISessionRepository`
- [ ] `SearchSessionsFunction` → `ISessionRepository`
- [ ] `SearchSessionsByEventFunction` → `ISessionRepository`
- [ ] `SearchSessionsByCveFunction` → `ISessionRepository`
### Rule Functions
- [ ] `GetRuleResultsFunction` → `IRuleRepository`
- [ ] `RunRulesFunction` → `IRuleRepository`
- [ ] Rule management functions → `IRuleRepository`
### Metrics Functions
- [ ] `GetMetricsFunction` → `IMetricsRepository`
- [ ] `GetHistoricalMetricsFunction` → `IMetricsRepository`
- [ ] `GetPlatformStatsFunction` → `IMetricsRepository`
### Maintenance Functions
- [ ] `GetAuditLogsFunction` → `IMaintenanceRepository`
- [ ] `MaintenanceFunction` → `IMaintenanceRepository`
### Vulnerability Functions
- [ ] All vulnerability functions → `IVulnerabilityRepository`
## Phase 3: Migrate "Bypass" Services
These services create their own `TableServiceClient` instances. Create proper repository implementations and migrate.
- [ ] `TenantConfigurationService` → `IConfigRepository`
- [ ] `AdminConfigurationService` → `IConfigRepository`
- [ ] `PreviewWhitelistService` → `IConfigRepository`
- [ ] `GlobalAdminService` → `IAdminRepository`
- [ ] `TenantAdminsService` → `IAdminRepository`
- [ ] `BootstrapSessionService` → `IBootstrapRepository`
- [ ] `GlobalNotificationService` → `INotificationRepository`
- [ ] `SessionReportService` → `INotificationRepository`
- [ ] `BlockedDeviceService` → `IDeviceSecurityRepository`
- [ ] `BlockedVersionService` → `IDeviceSecurityRepository`
- [ ] `ApiKeyMiddleware` → `IAdminRepository`
## Phase 4: Remove Legacy Code
- [ ] Remove `GetTableClient()` and `GetTableServiceClient()` from `TableStorageService`
- [ ] Move `TableStorageService` logic into repository implementations
- [ ] Delete `TableStorageService` partial files once fully migrated
- [ ] Remove direct `TableServiceClient` creation from all services
## Phase 5: Event Streaming (When Ready)
- [ ] Implement `EventHubPublisher` (or `ServiceBusPublisher`)
- [ ] Register via `services.AddEventStreaming<EventHubPublisher>()`
- [ ] Define event schemas for key domain events
- [ ] Add event consumers for downstream processing
## Phase 6: Cosmos DB Migration (When Ready)
- [ ] Create `CosmosSessionRepository`, `CosmosRuleRepository`, etc.
- [ ] Create `AddCosmosDataAccess()` extension method
- [ ] Replace inverted-tick indexing with native ORDER BY DESC
- [ ] Replace denormalized indexes with Cosmos queries
- [ ] Handle 2MB document limit vs. 64KB Table Storage limit
- [ ] Swap `AddTableStorageDataAccess()` → `AddCosmosDataAccess()` in Program.cs