Files
AutopilotMonitor/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetTenantConfigurationFunction.cs
T
Oliver Kieselbach 9ec8644be2 feat(settings): operator read-only settings view with real values
Operators previously saw editable settings forms bound to hardcoded defaults
whose Save always 403'd. Now they see the tenant's real configuration
read-only, with no save affordances at all.

Backend:
- GET config/{tenantId}: TenantAdminOrGlobalReader -> MemberRead. Safe because
  the handler redacts by default (CanViewSecrets: only GA or own-tenant Admin
  see secrets); Operators/Viewers receive RedactedCopyForReader().
- POST diagnostics/files: TenantAdminOrGA -> TenantAdminOrOperator (submitting
  logs is a troubleshooting action; Viewer and read-only GlobalReader excluded).
- Tests: policy pins updated; new middleware tests (own-tenant Operator allowed
  + redacted, cross-tenant Operator 403); CanViewSecrets theory for
  Operator/Viewer.

Web:
- TenantConfigContext loads the full config for all settings users (403
  fallback to feature-flags minimal config for deploy-order safety) and
  exposes canEditConfig; saveConfiguration guards on it.
- New ReadOnlyFieldset wraps section content (native fieldset disable, values
  stay copyable); SaveResetBar is not rendered at all for read-only viewers.
  Applied to Contact, SLA Targets, Hardware Whitelist (+ rejection-insights
  toggle), Notifications, Agent Settings, Agent Analyzers, Diagnostics, Data
  Management.
- Hidden entirely from Operators (in-page admin-only card + sidebar sub-item
  gate): Autopilot Validation, Access Management, Offboarding, Unrestricted
  Mode. GlobalReader sidebar parity kept via hasGlobalScope.

Tests: backend 3257/3257, web vitest 627/627, tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 18:40:27 +02:00

86 lines
4.4 KiB
C#

using System;
using System.Net;
using System.Threading.Tasks;
using AutopilotMonitor.Functions.Helpers;
using AutopilotMonitor.Functions.Services;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
namespace AutopilotMonitor.Functions.Functions.Config
{
public class GetTenantConfigurationFunction
{
private readonly ILogger<GetTenantConfigurationFunction> _logger;
private readonly TenantConfigurationService _configService;
public GetTenantConfigurationFunction(
ILogger<GetTenantConfigurationFunction> logger,
TenantConfigurationService configService)
{
_logger = logger;
_configService = configService;
}
[Function("GetTenantConfiguration")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "config/{tenantId}")] HttpRequestData req,
string tenantId)
{
try
{
// Authentication + MemberRead authorization enforced by PolicyEnforcementMiddleware —
// Operators/Viewers are admitted for a READ-ONLY view (secrets redacted below).
var requestCtx = req.GetRequestContext();
var userIdentifier = requestCtx.UserPrincipalName;
_logger.LogInformation($"GetTenantConfiguration: {requestCtx.TargetTenantId} by user {userIdentifier}");
var config = await _configService.GetConfigurationAsync(requestCtx.TargetTenantId);
// Only a caller with WRITE authority over THIS target tenant sees the full config including
// DiagnosticsBlobSasUrl / webhook URLs / custom headers (they manage those in the Settings
// UI). Everyone else admitted to this read tier — an own-tenant Operator/Viewer, a
// cross-tenant Global Reader, or a delegated reader viewing one of their managed
// tenants — gets the secrets redacted (returns
// a copy; the cached instance is never mutated). Redact-by-default: only Global Admin (any
// tenant) or the tenant's OWN admin clears secrets. The own-tenant check (target == JWT
// tenant) is what keeps a delegated reader — whose target is a DIFFERENT tenant — redacted
// even when they happen to be an admin of their own home tenant. This also prevents a
// "***REDACTED***" placeholder from ever reaching the Settings save round-trip for an
// own-tenant admin.
if (!CanViewSecrets(requestCtx))
config = config.RedactedCopyForReader();
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(config);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting configuration for tenant {TenantId}", tenantId);
var response = req.CreateResponse(HttpStatusCode.InternalServerError);
await response.WriteAsJsonAsync(new { error = "Internal server error" });
return response;
}
}
/// <summary>
/// Redact-by-default secret clearance for the config GET. Only a caller with WRITE authority over
/// THIS target tenant sees the unredacted secrets (SAS URLs, webhook URLs, custom headers): a Global
/// Admin (any tenant), or the tenant's OWN admin. Everyone else admitted to this read tier — an
/// own-tenant Operator/Viewer (read-only Settings view), a cross-tenant Global Reader, or a
/// delegated reader viewing one of their managed tenants — is
/// redacted. The own-tenant check (target == JWT tenant) is what keeps a delegated reader redacted
/// even when they are an admin of their own home tenant, since their target is a DIFFERENT tenant.
/// Extracted as a pure static so the security decision is unit-testable without an HttpRequestData mock.
/// </summary>
internal static bool CanViewSecrets(RequestContext ctx)
{
var ownTenantAdminView = ctx.IsTenantAdmin
&& string.Equals(ctx.TargetTenantId, ctx.TenantId, StringComparison.OrdinalIgnoreCase);
return ctx.IsGlobalAdmin || ownTenantAdminView;
}
}
}