186 lines
6.4 KiB
C#
186 lines
6.4 KiB
C#
using w4c_workflows.Models.Nodes;
|
|
using w4c_workflows.Services.Nodes;
|
|
using w4c_workflows.Services.Nodes.Executors;
|
|
using w4c_workflows.Services.Nodes.Interpolation;
|
|
using w4c_workflows.Services.Security;
|
|
using Xunit;
|
|
|
|
namespace w4c_workflows.Tests;
|
|
|
|
/// <summary>
|
|
/// End-to-end enforcement of node permissions: the same policy has to reject a
|
|
/// barred node at compile time and again at run time, and a run-time denial must
|
|
/// behave like any other node failure (error port / continue-on-fail).
|
|
/// </summary>
|
|
public class NodePermissionEnforcementTests
|
|
{
|
|
private sealed class StubExecutor : INodeExecutor
|
|
{
|
|
public StubExecutor(string type) => Type = type;
|
|
|
|
public string Type { get; }
|
|
|
|
public bool Invoked { get; private set; }
|
|
|
|
public Task<NodeExecutionOutcome> RunAsync(NodeExecutionContext context, CancellationToken ct)
|
|
{
|
|
Invoked = true;
|
|
return Task.FromResult(NodeExecutionOutcome.Single(context.Input(0)));
|
|
}
|
|
}
|
|
|
|
private static NodeGraphRunner Runner(NodePermissionPolicy permissions, params INodeExecutor[] extra)
|
|
{
|
|
var executors = new List<INodeExecutor> { new NoOpNodeExecutor() };
|
|
executors.AddRange(extra);
|
|
return new NodeGraphRunner(
|
|
new NodeExecutorRegistry(executors),
|
|
new NodeParameterInterpolator(),
|
|
logger: null,
|
|
permissions: permissions);
|
|
}
|
|
|
|
private static NodeGraph Compile(string yaml, string? tenantId = null, NodePermissionPolicy? permissions = null)
|
|
{
|
|
var result = new NodeGraphCompiler(NodeTestData.CoreCatalog(), permissions).Compile(NodeTestData.Parse(yaml), tenantId);
|
|
Assert.True(result.Success, string.Join("\n", result.Errors));
|
|
return result.Graph!;
|
|
}
|
|
|
|
// ------------------------------------------------------------------ run time
|
|
|
|
[Fact]
|
|
public async Task A_denied_node_fails_the_run_with_the_policy_code()
|
|
{
|
|
var yaml = """
|
|
name: denied
|
|
tasks:
|
|
- id: work
|
|
node: { type: core.noop }
|
|
""";
|
|
|
|
var policy = EgressTestData.Permissions(o => o.DenyTypes.Add("core.noop"));
|
|
var result = await Runner(policy).RunAsync(Compile(yaml), Array.Empty<FlowItem>());
|
|
|
|
Assert.False(result.Succeeded);
|
|
Assert.Equal("node_type_blocked", result.Failure!.Code);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_denied_node_routes_to_its_error_output_without_invoking_the_executor()
|
|
{
|
|
var yaml = """
|
|
name: denied-error-branch
|
|
tasks:
|
|
- id: work
|
|
node: { type: test.denied }
|
|
onError: recover
|
|
- id: recover
|
|
node: { type: core.noop }
|
|
""";
|
|
|
|
var catalog = NodeTestData.Catalog(
|
|
NodeTestData.NodeBlueprint("test.denied", outputs: new List<NodePort> { NodePort.Main, NodePort.Error }));
|
|
var compile = new NodeGraphCompiler(catalog).Compile(NodeTestData.Parse(yaml));
|
|
Assert.True(compile.Success, string.Join("\n", compile.Errors));
|
|
|
|
var executor = new StubExecutor("test.denied");
|
|
var policy = EgressTestData.Permissions(o => o.DenyTypes.Add("test.denied"));
|
|
|
|
var result = await Runner(policy, executor).RunAsync(compile.Graph!, new[] { FlowItem.FromJson(new System.Text.Json.Nodes.JsonObject()) });
|
|
|
|
Assert.True(result.Succeeded, result.Failure?.Message);
|
|
Assert.False(executor.Invoked);
|
|
var recovered = Assert.Single(result.OutputOf("recover"));
|
|
Assert.Equal("node_type_blocked", recovered.Json["error"]!["code"]!.GetValue<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_denied_node_with_continue_on_fail_does_not_fail_the_run()
|
|
{
|
|
var yaml = """
|
|
name: denied-soft
|
|
tasks:
|
|
- id: work
|
|
node: { type: core.noop }
|
|
continueOnFail: true
|
|
""";
|
|
|
|
var denying = EgressTestData.Permissions(o => o.DenyTypes.Add("core.noop"));
|
|
var result = await Runner(denying).RunAsync(Compile(yaml), Array.Empty<FlowItem>());
|
|
|
|
Assert.True(result.Succeeded, result.Failure?.Message);
|
|
Assert.Empty(result.OutputOf("work"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_permitted_run_is_unaffected_by_the_policy()
|
|
{
|
|
var yaml = """
|
|
name: allowed
|
|
tasks:
|
|
- id: work
|
|
node: { type: core.noop }
|
|
""";
|
|
|
|
var policy = EgressTestData.Permissions(o => o.AllowTypes.Add("core.*"));
|
|
var result = await Runner(policy).RunAsync(Compile(yaml), Array.Empty<FlowItem>());
|
|
|
|
Assert.True(result.Succeeded, result.Failure?.Message);
|
|
}
|
|
|
|
// ------------------------------------------------------------------ compile time
|
|
|
|
[Fact]
|
|
public void Compiler_rejects_a_barred_node_for_the_tenant()
|
|
{
|
|
var yaml = """
|
|
name: compile-denied
|
|
tasks:
|
|
- id: work
|
|
node: { type: core.noop }
|
|
""";
|
|
|
|
var policy = EgressTestData.Permissions(o => o.DenyTypes.Add("core.noop"));
|
|
var result = new NodeGraphCompiler(NodeTestData.CoreCatalog(), policy)
|
|
.Compile(NodeTestData.Parse(yaml), "tenant-a");
|
|
|
|
Assert.False(result.Success);
|
|
Assert.Contains(result.Errors, e => e.Contains("core.noop") && e.Contains("blocked"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Compiler_without_a_policy_permits_every_blueprint()
|
|
{
|
|
var yaml = """
|
|
name: compile-open
|
|
tasks:
|
|
- id: work
|
|
node: { type: core.noop }
|
|
""";
|
|
|
|
Assert.True(new NodeGraphCompiler(NodeTestData.CoreCatalog()).Compile(NodeTestData.Parse(yaml)).Success);
|
|
}
|
|
|
|
[Fact]
|
|
public void Compiler_applies_the_tenant_rule_not_the_global_one()
|
|
{
|
|
var yaml = """
|
|
name: compile-tenant
|
|
tasks:
|
|
- id: work
|
|
node: { type: core.noop }
|
|
""";
|
|
|
|
var policy = EgressTestData.Permissions(o =>
|
|
{
|
|
o.DenyTypes.Add("core.noop");
|
|
o.Tenants["privileged"] = new NodePermissionRule();
|
|
});
|
|
var compiler = new NodeGraphCompiler(NodeTestData.CoreCatalog(), policy);
|
|
|
|
Assert.True(compiler.Compile(NodeTestData.Parse(yaml), "privileged").Success);
|
|
Assert.False(compiler.Compile(NodeTestData.Parse(yaml), "regular").Success);
|
|
}
|
|
}
|