194 lines
7 KiB
C#
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));
|
|
}
|