w4c-workflows-api/w4c-workflows-api.Tests/NodePermissionPolicyTests.cs
2026-09-12 01:02:46 +03:00

194 lines
7 KiB
C#

using w4c_workflows.Models.Nodes;
using w4c_workflows.Services.Security;
using Xunit;
namespace w4c_workflows.Tests;
/// <summary>
/// Pure policy tests: no catalog, no run. Every decision is driven by a
/// fabricated blueprint so the allow/deny order and the wildcard matcher are
/// exercised directly.
/// </summary>
public class NodePermissionPolicyTests
{
private static NodeBlueprint Blueprint(
string type,
string kind = NodeKind.Action,
string origin = NodeOrigin.BuiltIn)
=> new()
{
Type = type,
Version = 1,
DisplayName = type,
Kind = kind,
Origin = origin,
};
[Fact]
public void Default_policy_permits_everything()
{
var policy = EgressTestData.Permissions();
Assert.True(policy.IsPermitted(Blueprint("core.noop")));
Assert.True(policy.IsPermitted(Blueprint("core.code")));
Assert.True(policy.IsPermitted(Blueprint("core.executeWorkflow", NodeKind.Control)));
Assert.True(policy.IsPermitted(Blueprint("slack", NodeKind.Action, NodeOrigin.Connector)));
}
// ------------------------------------------------------------------ type lists
[Theory]
[InlineData("core.code")]
[InlineData("core.httpRequest")]
[InlineData("slack")]
public void Deny_types_blocks_exact_and_wildcard_matches(string type)
{
var policy = EgressTestData.Permissions(o => o.DenyTypes.AddRange(new[] { "core.*", "slack" }));
var decision = policy.Evaluate(Blueprint(type));
Assert.False(decision.Allowed);
Assert.Equal("node_type_blocked", decision.Code);
Assert.Contains(type, decision.Reason);
}
[Fact]
public void Deny_types_leaves_other_types_untouched()
{
var policy = EgressTestData.Permissions(o => o.DenyTypes.Add("core.code"));
Assert.True(policy.IsPermitted(Blueprint("core.noop")));
}
[Fact]
public void Allow_types_is_a_whitelist()
{
var policy = EgressTestData.Permissions(o => o.AllowTypes.Add("core.set"));
Assert.True(policy.IsPermitted(Blueprint("core.set")));
var denied = policy.Evaluate(Blueprint("core.noop"));
Assert.False(denied.Allowed);
Assert.Equal("node_type_not_permitted", denied.Code);
}
[Fact]
public void Deny_wins_over_allow()
{
var policy = EgressTestData.Permissions(o =>
{
o.AllowTypes.Add("core.*");
o.DenyTypes.Add("core.code");
});
Assert.True(policy.IsPermitted(Blueprint("core.set")));
Assert.False(policy.IsPermitted(Blueprint("core.code")));
}
// ------------------------------------------------------------------ kind / origin
[Fact]
public void Deny_kind_blocks_that_role()
{
var policy = EgressTestData.Permissions(o => o.DenyKinds.Add(NodeKind.Ai));
var denied = policy.Evaluate(Blueprint("ai.agent", NodeKind.Ai));
Assert.False(denied.Allowed);
Assert.Equal("node_kind_blocked", denied.Code);
Assert.True(policy.IsPermitted(Blueprint("core.set", NodeKind.Transform)));
}
[Fact]
public void Allow_kind_is_a_whitelist()
{
var policy = EgressTestData.Permissions(o => o.AllowKinds.Add(NodeKind.Control));
Assert.True(policy.IsPermitted(Blueprint("core.if", NodeKind.Control)));
var denied = policy.Evaluate(Blueprint("core.httpRequest", NodeKind.Action));
Assert.False(denied.Allowed);
Assert.Equal("node_kind_not_permitted", denied.Code);
}
[Fact]
public void Origin_deny_and_allow_are_enforced()
{
var deny = EgressTestData.Permissions(o => o.DenyOrigins.Add(NodeOrigin.Custom));
var blocked = deny.Evaluate(Blueprint("custom.node", NodeKind.Action, NodeOrigin.Custom));
Assert.False(blocked.Allowed);
Assert.Equal("node_origin_blocked", blocked.Code);
var allow = EgressTestData.Permissions(o => o.AllowOrigins.Add(NodeOrigin.BuiltIn));
var notPermitted = allow.Evaluate(Blueprint("slack", NodeKind.Action, NodeOrigin.Connector));
Assert.False(notPermitted.Allowed);
Assert.Equal("node_origin_not_permitted", notPermitted.Code);
}
// ------------------------------------------------------------------ convenience switches
[Fact]
public void Code_nodes_can_be_disabled_without_touching_other_nodes()
{
var policy = EgressTestData.Permissions(o => o.AllowCodeNodes = false);
var denied = policy.Evaluate(Blueprint("core.code", NodeKind.Transform));
Assert.False(denied.Allowed);
Assert.Equal("code_node_disabled", denied.Code);
Assert.True(policy.IsPermitted(Blueprint("core.noop")));
}
[Fact]
public void Sub_workflow_nodes_can_be_disabled()
{
var policy = EgressTestData.Permissions(o => o.AllowSubWorkflows = false);
var denied = policy.Evaluate(Blueprint("core.executeWorkflow", NodeKind.Control));
Assert.False(denied.Allowed);
Assert.Equal("sub_workflow_disabled", denied.Code);
Assert.True(policy.IsPermitted(Blueprint("core.noop")));
}
[Fact]
public void Explicit_deny_beats_the_code_switch_allow()
{
// AllowCodeNodes true, but a deny entry still blocks it (deny wins).
var policy = EgressTestData.Permissions(o => o.DenyTypes.Add("core.code"));
Assert.False(policy.IsPermitted(Blueprint("core.code")));
}
// ------------------------------------------------------------------ tenant overrides
[Fact]
public void Tenant_rule_replaces_the_global_rule()
{
var policy = EgressTestData.Permissions(o =>
{
o.DenyTypes.Add("core.code");
o.Tenants["locked-down"] = new NodePermissionRule { AllowCodeNodes = false };
o.Tenants["open"] = new NodePermissionRule(); // permissive override
});
// Global rule bars code.
Assert.False(policy.IsPermitted(Blueprint("core.code"), "someone-else"));
// The permissive tenant rule lifts the global bar.
Assert.True(policy.IsPermitted(Blueprint("core.code"), "open"));
// A tenant can be stricter than the global rule: code is barred there
// even though the global rule would allow it.
Assert.False(policy.IsPermitted(Blueprint("core.code"), "locked-down"));
Assert.True(policy.IsPermitted(Blueprint("core.noop"), "locked-down"));
}
// ------------------------------------------------------------------ matcher
[Theory]
[InlineData("core.set", "core.*", true)]
[InlineData("core", "core.*", false)]
[InlineData("core.code", "*.code", true)]
[InlineData("core.set", "*.code", false)]
[InlineData("anything", "*", true)]
[InlineData("core.set", "core.set", true)]
[InlineData("CORE.SET", "core.set", true)]
[InlineData("core.set", "core", false)]
public void Glob_matcher_is_anchored_and_case_insensitive(string value, string pattern, bool expected)
=> Assert.Equal(expected, NodePermissionPolicy.MatchesPattern(value, pattern));
}