mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-25 05:00:07 +00:00
9ec8644be2
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>
86 lines
4.4 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|