using System.Security.Claims;
using Bunit;
using Bunit.TestDoubles;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.Auth.AspNetCore;
using ZB.MOM.WW.Secrets.Abstractions;
using ZB.MOM.WW.Secrets.Ui.Components;
using ZB.MOM.WW.Secrets.Ui.Tests.Fakes;
namespace ZB.MOM.WW.Secrets.Ui.Tests;
///
/// Secrets-016: each public component re-checks its own policy, so a host that renders one outside
/// an AuthorizeView cannot hand its action to a user without the right; and the REAL policy
/// definitions in are pinned (the bUnit
/// tests elsewhere use SetPolicies, which bypasses them).
///
public class ComponentAuthorizationTests : TestContext
{
private const string PlainValue = "the-plaintext-value-42";
private (RecordingSecretStore Store, CapturingAuditWriter Audit, FakeCipher Cipher) Setup(params string[] grantedPolicies)
{
var store = new RecordingSecretStore();
var audit = new CapturingAuditWriter();
var cipher = new FakeCipher();
Services.AddSingleton(store);
Services.AddSingleton(audit);
Services.AddSingleton(cipher);
var auth = this.AddTestAuthorization();
auth.SetAuthorized("eve");
auth.SetPolicies(grantedPolicies);
return (store, audit, cipher);
}
[Fact]
public void RevealButton_outside_an_AuthorizeView_refuses_a_manager_without_reveal()
{
// A host composed the button into a page gated only by ManagePolicy.
var (store, audit, cipher) = Setup(SecretsAuthorization.ManagePolicy);
var name = new SecretName("prod/db");
store.Seed(cipher.Encrypt(name, PlainValue, SecretContentType.Text) with
{
Revision = 1, CreatedUtc = DateTimeOffset.UnixEpoch, UpdatedUtc = DateTimeOffset.UnixEpoch,
});
var cut = RenderComponent(p => p.Add(c => c.Name, name));
cut.Find("button.reveal-secret").Click();
Assert.DoesNotContain(PlainValue, cut.Markup);
Assert.Contains("not authorized", cut.Find("[data-testid=reveal-error]").TextContent);
Assert.DoesNotContain(audit.Events, e => e.Outcome == AuditOutcome.Success);
}
[Fact]
public void RevealButton_still_reveals_for_a_holder_of_the_reveal_policy()
{
var (store, _, cipher) = Setup(SecretsAuthorization.RevealPolicy);
var name = new SecretName("prod/db");
store.Seed(cipher.Encrypt(name, PlainValue, SecretContentType.Text) with
{
Revision = 1, CreatedUtc = DateTimeOffset.UnixEpoch, UpdatedUtc = DateTimeOffset.UnixEpoch,
});
var cut = RenderComponent(p => p.Add(c => c.Name, name));
cut.Find("button.reveal-secret").Click();
Assert.Contains(PlainValue, cut.Markup);
}
[Fact]
public void SecretEditor_refuses_a_user_without_the_manage_policy()
{
var (store, audit, _) = Setup(/* no policies */);
var cut = RenderComponent(p => p.Add(c => c.OnSaved, () => { }));
cut.Find("[data-testid=secret-name]").Change("app/thing");
cut.Find("[data-testid=secret-value]").Change("v");
cut.Find("[data-testid=editor-submit]").Click();
Assert.Empty(store.Upserts);
Assert.Empty(audit.Events);
Assert.Contains("not authorized", cut.Find("[data-testid=editor-error]").TextContent);
}
[Fact]
public void ConfirmDeleteModal_refuses_a_user_without_the_manage_policy()
{
var (store, _, cipher) = Setup(/* no policies */);
var name = new SecretName("app/thing");
store.Seed(cipher.Encrypt(name, "v", SecretContentType.Text) with
{
Revision = 1, CreatedUtc = DateTimeOffset.UnixEpoch, UpdatedUtc = DateTimeOffset.UnixEpoch,
});
bool deleted = false;
var cut = RenderComponent(p => p
.Add(c => c.Visible, true)
.Add(c => c.Name, name)
.Add(c => c.OnDeleted, () => deleted = true));
cut.Find("[data-testid=confirm-delete]").Click();
Assert.Empty(store.Deletes);
Assert.False(deleted);
}
// ---------- the real policy definitions ----------
public static TheoryData RoleMatrix => new()
{
// role, satisfies ManagePolicy, satisfies RevealPolicy
{ SecretsAuthorization.ManageRole, true, false },
{ SecretsAuthorization.RevealRole, true, true },
{ SecretsAuthorization.AdminRole, true, true },
{ "Viewer", false, false },
{ null, false, false },
};
[Theory]
[MemberData(nameof(RoleMatrix))]
public async Task The_policy_definitions_grant_exactly_the_documented_roles(string? role, bool manage, bool reveal)
{
var services = new ServiceCollection();
services.AddLogging();
services.AddAuthorization(o => o.AddSecretsAuthorization());
await using ServiceProvider sp = services.BuildServiceProvider();
var authorization = sp.GetRequiredService();
var claims = new List { new(ClaimTypes.Name, "u") };
if (role is not null)
{
claims.Add(new Claim(ZbClaimTypes.Role, role));
}
var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "test", ClaimTypes.Name, ZbClaimTypes.Role));
Assert.Equal(manage, (await authorization.AuthorizeAsync(user, SecretsAuthorization.ManagePolicy)).Succeeded);
Assert.Equal(reveal, (await authorization.AuthorizeAsync(user, SecretsAuthorization.RevealPolicy)).Succeeded);
}
[Fact]
public async Task An_unauthenticated_principal_satisfies_neither_policy_even_with_the_role_claim()
{
var services = new ServiceCollection();
services.AddLogging();
services.AddAuthorization(o => o.AddSecretsAuthorization());
await using ServiceProvider sp = services.BuildServiceProvider();
var authorization = sp.GetRequiredService();
// No authentication type -> IsAuthenticated == false.
var user = new ClaimsPrincipal(new ClaimsIdentity(
[new Claim(ZbClaimTypes.Role, SecretsAuthorization.AdminRole)], authenticationType: null,
ClaimTypes.Name, ZbClaimTypes.Role));
Assert.False((await authorization.AuthorizeAsync(user, SecretsAuthorization.ManagePolicy)).Succeeded);
Assert.False((await authorization.AuthorizeAsync(user, SecretsAuthorization.RevealPolicy)).Succeeded);
}
}