Files
AutopilotMonitor/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetImeVersionHistoryFunction.cs
T
Oliver Kieselbach 2d3853c38f feat(rbac): add read-only GlobalReader platform role (backend + MCP + web)
GlobalReader = cross-tenant READ visibility like a Global Admin, but no mutating
operations and no platform settings. Provisioned exactly like a GA: a GlobalAdmins
table row with Role=GlobalReader (empty/legacy Role = GlobalAdmin, back-compat).
Semantics are ADDITIVE — a user who is also their own tenant's Admin keeps edit
rights on that tenant; a pure GlobalReader is read-only everywhere.

Backend:
- GlobalAdmins.Role column + GlobalAdminService.GetGlobalRoleAsync; IsGlobalAdminAsync
  (incl. TableAdminRepository) delegates so a GlobalReader row never reads back as admin
- RequestContext.IsGlobalReader + HasGlobalScope
- Split GlobalAdminOnly into GlobalReadOrAdmin (cross-tenant reads) vs GlobalAdminOnly
  (writes/settings) + TenantAdminOrGlobalReader for config-read
- PolicyEnforcementMiddleware: extracted testable DecideAsync seam; cross-tenant gate keys
  off HasGlobalScope; additive own-tenant role carried so IsTenantAdmin stays correct
- Config secret redaction for readers (RedactedCopyForReader) + save-time placeholder
  restore (RestoreRedactedSecretsFrom) so a redacted view can't blank real secrets
- raw table/log tools stay GlobalAdminOnly; SignalR global-admins read group admits HasGlobalScope
- /auth/me emits isGlobalReader (+ preview-gate bypass, no auto-admin); /auth/mcp emits globalRole

MCP: hasGlobalScope (GA or Reader) drives routing + tool catalog; raw secret-bearing tools
(query_table/list_tables/query_backend_logs) gated strict-GA only.

Web: AuthContext isGlobalReader + hasGlobalScope; reader reaches /dashboard + cross-tenant
views; platform Settings/Ops/Backups/Software GA-only (nav-hidden + requireGlobalAdmin route
layouts); mutating controls on mixed data pages disabled via useCanMutatePlatform; rules pages
read-only on foreign tenants; additive own-tenant edit preserved.

Tests: backend 2369, MCP 252, web 469 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 19:07:10 +02:00

67 lines
2.5 KiB
C#

using System.Net;
using AutopilotMonitor.Functions.Helpers;
using AutopilotMonitor.Shared.DataAccess;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
namespace AutopilotMonitor.Functions.Functions.Metrics
{
/// <summary>
/// Returns all known IME agent versions with first/last seen dates and session counts.
/// Permanent archive that survives data retention — tracks Microsoft IME releases over time.
/// Access: MemberRead (any tenant member can see version/dates/counts).
/// FirstSeenTenantId and FirstSeenSessionId are only visible to Global Admins.
/// </summary>
public class GetImeVersionHistoryFunction
{
private readonly ILogger<GetImeVersionHistoryFunction> _logger;
private readonly ISessionRepository _sessionRepo;
public GetImeVersionHistoryFunction(ILogger<GetImeVersionHistoryFunction> logger, ISessionRepository sessionRepo)
{
_logger = logger;
_sessionRepo = sessionRepo;
}
[Function("GetImeVersionHistory")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "metrics/ime-versions")] HttpRequestData req)
{
try
{
var requestCtx = req.GetRequestContext();
var versions = await _sessionRepo.GetImeVersionHistoryAsync();
var response = req.CreateResponse(HttpStatusCode.OK);
if (requestCtx.HasGlobalScope)
{
await response.WriteAsJsonAsync(versions);
}
else
{
// Strip sensitive cross-tenant data for non-Global Admins
var redacted = versions.Select(v => new
{
v.Version,
v.FirstSeenAt,
v.LastSeenAt,
v.SessionCount
});
await response.WriteAsJsonAsync(redacted);
}
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching IME version history");
var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError);
await errorResponse.WriteAsJsonAsync(new { error = "Failed to retrieve IME version history" });
return errorResponse;
}
}
}
}