mirror of
https://github.com/juanfont/headscale.git
synced 2026-09-18 06:22:05 +09:00
@@ -483,3 +483,124 @@ func testACLSuccess(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestACLCompatGlobalEquivalence verifies that the global filter
|
||||||
|
// compilation path (PolicyManager.FilterForNode) produces the same
|
||||||
|
// output as the per-node path (compileFilterRulesForNode +
|
||||||
|
// ReduceFilterRules) for all ACL golden files that use ACL-only
|
||||||
|
// policies (no autogroup:self, no via grants).
|
||||||
|
//
|
||||||
|
// Files using autogroup:self are skipped because they correctly
|
||||||
|
// require per-node compilation (needsPerNodeFilter=true), where
|
||||||
|
// the global path intentionally diverges.
|
||||||
|
func TestACLCompatGlobalEquivalence(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
files, err := filepath.Glob(
|
||||||
|
filepath.Join("testdata", "acl_results", "ACL-*.hujson"),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, files)
|
||||||
|
|
||||||
|
users := setupACLCompatUsers()
|
||||||
|
nodes := setupACLCompatNodes(users)
|
||||||
|
|
||||||
|
tested := 0
|
||||||
|
skippedSelf := 0
|
||||||
|
skippedErr := 0
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
tf := loadACLTestFile(t, file)
|
||||||
|
|
||||||
|
t.Run(tf.TestID, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if reason, ok := aclSkipReasons[tf.TestID]; ok {
|
||||||
|
t.Skipf("TODO: %s", reason)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if tf.Error {
|
||||||
|
skippedErr++
|
||||||
|
|
||||||
|
t.Skip("error case, not applicable")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
policyJSON := convertPolicyUserEmails(
|
||||||
|
tf.Input.FullPolicy,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check if policy uses autogroup:self — if so, the
|
||||||
|
// global and per-node paths intentionally diverge.
|
||||||
|
if strings.Contains(string(policyJSON), "autogroup:self") {
|
||||||
|
skippedSelf++
|
||||||
|
|
||||||
|
t.Skip(
|
||||||
|
"uses autogroup:self, global path " +
|
||||||
|
"intentionally diverges",
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-node path.
|
||||||
|
pol, err := unmarshalPolicy(policyJSON)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = pol.validate()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Global path.
|
||||||
|
pm, err := NewPolicyManager(
|
||||||
|
policyJSON, users, nodes.ViewSlice(),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for _, node := range nodes {
|
||||||
|
nv := node.View()
|
||||||
|
|
||||||
|
perNodeRules, err := pol.compileFilterRulesForNode(
|
||||||
|
users, nv, nodes.ViewSlice(),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
perNodeReduced := policyutil.ReduceFilterRules(
|
||||||
|
nv, perNodeRules,
|
||||||
|
)
|
||||||
|
|
||||||
|
globalReduced, err := pm.FilterForNode(nv)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
opts := append(
|
||||||
|
cmpOptions(),
|
||||||
|
cmpopts.EquateEmpty(),
|
||||||
|
)
|
||||||
|
if diff := cmp.Diff(
|
||||||
|
perNodeReduced,
|
||||||
|
globalReduced,
|
||||||
|
opts...,
|
||||||
|
); diff != "" {
|
||||||
|
t.Errorf(
|
||||||
|
"%s/%s: global vs per-node "+
|
||||||
|
"filter mismatch "+
|
||||||
|
"(-perNode +global):\n%s",
|
||||||
|
tf.TestID,
|
||||||
|
node.GivenName,
|
||||||
|
diff,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tested++
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf(
|
||||||
|
"ACL global equivalence: tested=%d, "+
|
||||||
|
"skippedSelf=%d, skippedErr=%d",
|
||||||
|
tested, skippedSelf, skippedErr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -618,3 +618,116 @@ func testGrantSuccess(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestGrantsCompatGlobalEquivalence verifies that the global filter
|
||||||
|
// compilation path (PolicyManager.FilterForNode) produces the same
|
||||||
|
// output as the per-node path (compileFilterRulesForNode +
|
||||||
|
// ReduceFilterRules) for all GRANT golden files that don't require
|
||||||
|
// per-node compilation.
|
||||||
|
//
|
||||||
|
// Files using via grants or autogroup:self are skipped because they
|
||||||
|
// correctly require per-node compilation (needsPerNodeFilter=true).
|
||||||
|
func TestGrantsCompatGlobalEquivalence(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
files, err := filepath.Glob(
|
||||||
|
filepath.Join("testdata", "grant_results", "GRANT-*.hujson"),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, files)
|
||||||
|
|
||||||
|
users := setupGrantsCompatUsers()
|
||||||
|
allNodes := setupGrantsCompatNodes(users)
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
tf := loadGrantTestFile(t, file)
|
||||||
|
|
||||||
|
t.Run(tf.TestID, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if reason, ok := grantSkipReasons[tf.TestID]; ok {
|
||||||
|
t.Skipf("TODO: %s", reason)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if tf.Error {
|
||||||
|
t.Skip("error case")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
policyJSON := convertPolicyUserEmails(
|
||||||
|
tf.Input.FullPolicy,
|
||||||
|
)
|
||||||
|
policyStr := string(policyJSON)
|
||||||
|
|
||||||
|
// Skip files that require per-node compilation.
|
||||||
|
if strings.Contains(policyStr, "autogroup:self") {
|
||||||
|
t.Skip("uses autogroup:self")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(policyStr, "\"via\"") {
|
||||||
|
t.Skip("uses via grants")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select nodes based on topology size.
|
||||||
|
nodes := allNodes
|
||||||
|
if _, hasNewNodes := tf.Captures["exit-a"]; !hasNewNodes {
|
||||||
|
nodes = allNodes[:8]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-node path.
|
||||||
|
pol, err := unmarshalPolicy(policyJSON)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = pol.validate()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Global path.
|
||||||
|
pm, err := NewPolicyManager(
|
||||||
|
policyJSON, users, nodes.ViewSlice(),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for _, node := range nodes {
|
||||||
|
nv := node.View()
|
||||||
|
|
||||||
|
perNodeRules, err := pol.compileFilterRulesForNode(
|
||||||
|
users, nv, nodes.ViewSlice(),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
perNodeReduced := policyutil.ReduceFilterRules(
|
||||||
|
nv, perNodeRules,
|
||||||
|
)
|
||||||
|
|
||||||
|
globalReduced, err := pm.FilterForNode(nv)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
opts := append(
|
||||||
|
cmpOptions(),
|
||||||
|
cmpopts.EquateEmpty(),
|
||||||
|
)
|
||||||
|
if diff := cmp.Diff(
|
||||||
|
perNodeReduced,
|
||||||
|
globalReduced,
|
||||||
|
opts...,
|
||||||
|
); diff != "" {
|
||||||
|
t.Errorf(
|
||||||
|
"%s/%s: global vs per-node "+
|
||||||
|
"filter mismatch "+
|
||||||
|
"(-perNode +global):\n%s",
|
||||||
|
tf.TestID,
|
||||||
|
node.GivenName,
|
||||||
|
diff,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -974,6 +974,100 @@ func TestRoutesCompatReduceRoutes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestRoutesCompatGlobalEquivalence verifies that the global filter
|
||||||
|
// compilation path (compileFilterRules + ReduceFilterRules, used in
|
||||||
|
// production for ACL-only policies) produces the same output as the
|
||||||
|
// per-node path (compileFilterRulesForNode + ReduceFilterRules, used
|
||||||
|
// by TestRoutesCompat).
|
||||||
|
//
|
||||||
|
// All 98 ROUTES golden files use ACL-only policies (no autogroup:self,
|
||||||
|
// no via grants), so production code takes the global path. The
|
||||||
|
// TestRoutesCompat test always uses the per-node path. If these two
|
||||||
|
// paths ever diverge, this test will detect it.
|
||||||
|
func TestRoutesCompatGlobalEquivalence(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
files, err := filepath.Glob(
|
||||||
|
filepath.Join("testdata", "routes_results", "ROUTES-*.hujson"),
|
||||||
|
)
|
||||||
|
require.NoError(t, err, "failed to glob test files")
|
||||||
|
require.NotEmpty(
|
||||||
|
t,
|
||||||
|
files,
|
||||||
|
"no ROUTES-*.hujson test files found",
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
tf := loadRoutesTestFile(t, file)
|
||||||
|
|
||||||
|
t.Run(tf.TestID, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if reason, ok := routesSkipReasons[tf.TestID]; ok {
|
||||||
|
t.Skipf("TODO: %s", reason)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
users, nodes := buildRoutesUsersAndNodes(t, tf.Topology)
|
||||||
|
policyJSON := convertPolicyUserEmails(tf.Input.FullPolicy)
|
||||||
|
|
||||||
|
// Per-node path: compile per-node, then reduce.
|
||||||
|
pol, err := unmarshalPolicy(policyJSON)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = pol.validate()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Global path: create PolicyManager (compiles global
|
||||||
|
// filter internally).
|
||||||
|
pm, err := NewPolicyManager(
|
||||||
|
policyJSON, users, nodes.ViewSlice(),
|
||||||
|
)
|
||||||
|
require.NoError(t, err,
|
||||||
|
"%s: failed to create PolicyManager", tf.TestID,
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, node := range nodes {
|
||||||
|
nv := node.View()
|
||||||
|
|
||||||
|
// Per-node path (what TestRoutesCompat uses).
|
||||||
|
perNodeRules, err := pol.compileFilterRulesForNode(
|
||||||
|
users, nv, nodes.ViewSlice(),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
perNodeReduced := policyutil.ReduceFilterRules(
|
||||||
|
nv, perNodeRules,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Global path (what production FilterForNode
|
||||||
|
// uses for ACL-only policies).
|
||||||
|
globalReduced, err := pm.FilterForNode(nv)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
opts := append(
|
||||||
|
cmpOptions(),
|
||||||
|
cmpopts.EquateEmpty(),
|
||||||
|
)
|
||||||
|
if diff := cmp.Diff(
|
||||||
|
perNodeReduced,
|
||||||
|
globalReduced,
|
||||||
|
opts...,
|
||||||
|
); diff != "" {
|
||||||
|
t.Errorf(
|
||||||
|
"%s/%s: global vs per-node filter "+
|
||||||
|
"mismatch (-perNode +global):\n%s",
|
||||||
|
tf.TestID,
|
||||||
|
node.GivenName,
|
||||||
|
diff,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestRoutesCompatNoFalsePositivePeers verifies that nodes which do NOT
|
// TestRoutesCompatNoFalsePositivePeers verifies that nodes which do NOT
|
||||||
// have subnet routes overlapping an ACL's source or destination CIDRs
|
// have subnet routes overlapping an ACL's source or destination CIDRs
|
||||||
// are NOT incorrectly peered with subnet routers.
|
// are NOT incorrectly peered with subnet routers.
|
||||||
|
|||||||
Reference in New Issue
Block a user