test(backend): cover delegated config/all subset + redaction seams [Phase 2b follow-up]

Addresses a Codex finding: no handler-level coverage existed for the delegated
config/all filtering/redaction path (coverage was policy-context + projection only).

Following the repo convention (handler HTTP entry not unit-tested — see
GetAllBlockedDevicesFunctionTests), the security-relevant transform is extracted
into two pure, testable seams on GetAllTenantConfigurationsFunction and tested
directly:
- ExistingManagedConfigs: from the per-tenant point-read results, keep only managed
  tenants that actually have a config row (exists==true); drop the rest. Tested:
  drops missing rows, keeps present ones, empty when none exist.
- DelegatedBareArrayView: redact every managed config's secrets (a delegated admin
  is never a Global Admin). Tested: secrets -> placeholder, non-secrets preserved,
  original instance not mutated.

5 new tests; full Functions suite green (2469).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-06-25 20:58:11 +02:00
parent 43a87ff747
commit 4bb6dd59f7
2 changed files with 114 additions and 3 deletions
@@ -0,0 +1,91 @@
using AutopilotMonitor.Functions.Functions.Config;
using AutopilotMonitor.Shared;
using AutopilotMonitor.Shared.Models;
using Xunit;
namespace AutopilotMonitor.Functions.Tests;
/// <summary>
/// Covers the delegated ("MSP") config/all transformation seams in
/// <see cref="GetAllTenantConfigurationsFunction"/>. The HTTP entry point is intentionally not exercised
/// (mocking HttpRequestData + the middleware chain is more setup than the test is worth — same rationale as
/// <see cref="GetAllBlockedDevicesFunctionTests"/>); the security-relevant logic is the pure subset/redaction
/// transform, which is tested directly.
///
/// Guards two invariants for a delegated caller: (1) only managed tenants that actually have a config row
/// are returned (no phantom/empty entries), and (2) per-tenant secrets are ALWAYS redacted (a delegated
/// admin is never a Global Admin, so it must never receive SAS/webhook/header secrets for a managed tenant).
/// </summary>
public class GetAllTenantConfigurationsFunctionTests
{
private const string TenantB = "22222222-2222-2222-2222-222222222222";
private const string TenantC = "33333333-3333-3333-3333-333333333333";
[Fact]
public void ExistingManagedConfigs_DropsTenantsWithoutConfigRow()
{
var reads = new (TenantConfiguration config, bool exists)[]
{
(new TenantConfiguration { TenantId = TenantB }, true),
(new TenantConfiguration { TenantId = TenantC }, false), // no config row → must be dropped
};
var result = GetAllTenantConfigurationsFunction.ExistingManagedConfigs(reads);
Assert.Single(result);
Assert.Equal(TenantB, result[0].TenantId);
}
[Fact]
public void ExistingManagedConfigs_AllExist_KeepsAll()
{
var reads = new (TenantConfiguration config, bool exists)[]
{
(new TenantConfiguration { TenantId = TenantB }, true),
(new TenantConfiguration { TenantId = TenantC }, true),
};
var result = GetAllTenantConfigurationsFunction.ExistingManagedConfigs(reads);
Assert.Equal(2, result.Count);
}
[Fact]
public void ExistingManagedConfigs_NoneExist_Empty()
{
var reads = new (TenantConfiguration config, bool exists)[]
{
(new TenantConfiguration { TenantId = TenantB }, false),
};
Assert.Empty(GetAllTenantConfigurationsFunction.ExistingManagedConfigs(reads));
}
[Fact]
public void DelegatedBareArrayView_RedactsSecretsButKeepsNonSecrets()
{
var managed = new[]
{
new TenantConfiguration
{
TenantId = TenantB,
DiagnosticsBlobSasUrl = "https://acct.blob.core.windows.net/c?sig=secret",
WebhookUrl = "https://hooks.example/abc",
DomainName = "contoso.com",
},
};
var view = GetAllTenantConfigurationsFunction.DelegatedBareArrayView(managed);
var v = Assert.Single(view);
Assert.Equal(Constants.RedactedSecretPlaceholder, v.DiagnosticsBlobSasUrl);
Assert.Equal(Constants.RedactedSecretPlaceholder, v.WebhookUrl);
Assert.Equal("contoso.com", v.DomainName); // non-secret preserved
// original instance is never mutated (redaction returns a copy)
Assert.Equal("https://hooks.example/abc", managed[0].WebhookUrl);
}
[Fact]
public void DelegatedBareArrayView_Empty_Empty()
=> Assert.Empty(GetAllTenantConfigurationsFunction.DelegatedBareArrayView(new TenantConfiguration[0]));
}
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
@@ -6,6 +7,7 @@ using System.Web;
using AutopilotMonitor.Functions.Helpers;
using AutopilotMonitor.Functions.Pagination;
using AutopilotMonitor.Functions.Services;
using AutopilotMonitor.Shared.Models;
using AutopilotMonitor.Shared.Pagination;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
@@ -58,15 +60,14 @@ namespace AutopilotMonitor.Functions.Functions.Config
// never writes a default row). exists==false drops any id with no config row.
var reads = await Task.WhenAll(requestCtx.AllowedTenantIds.Select(async tid =>
await _configService.TryGetConfigurationAsync(tid)));
var subset = reads.Where(r => r.exists).Select(r => r.config).ToList();
var subset = ExistingManagedConfigs(reads);
_logger.LogInformation("GetAllTenantConfigurations (delegated subset, {Count} tenants) by {User}",
subset.Count, userIdentifier);
if (parsed.PageSize == null)
{
var redacted = subset.Select(c => c.RedactedCopyForReader()).ToList();
var resp = req.CreateResponse(HttpStatusCode.OK);
await resp.WriteAsJsonAsync(redacted);
await resp.WriteAsJsonAsync(DelegatedBareArrayView(subset));
return resp;
}
@@ -149,5 +150,24 @@ namespace AutopilotMonitor.Functions.Functions.Config
return response;
}
}
/// <summary>
/// From the per-tenant point-read results, keep only the managed tenants that actually HAVE a config
/// row (exists). A delegated caller's AllowedTenantIds is the authoritative bound; an id with no row
/// (offboarded / never onboarded) is silently dropped rather than surfaced as an empty default.
/// Pure + testable seam (handler HTTP entry is not unit-tested — see GetAllBlockedDevicesFunctionTests).
/// </summary>
internal static List<TenantConfiguration> ExistingManagedConfigs(
IEnumerable<(TenantConfiguration config, bool exists)> reads)
=> reads.Where(r => r.exists).Select(r => r.config).ToList();
/// <summary>
/// The delegated bare-array view of config/all: every managed-tenant config with its secrets
/// (SAS / webhook URLs / custom headers) redacted. A delegated admin is never a Global Admin, so it
/// must NEVER receive unredacted secrets for a tenant it merely manages. Pure + testable.
/// </summary>
internal static List<TenantConfiguration> DelegatedBareArrayView(
IEnumerable<TenantConfiguration> existingManaged)
=> existingManaged.Select(c => c.RedactedCopyForReader()).ToList();
}
}