mirror of
https://github.com/juanfont/headscale.git
synced 2026-09-17 05:52:17 +09:00
policy/v2: make ACL error validation strict
Replace the lenient testACLError handler (t.Logf on mismatch) with strict validation matching the grant error handler pattern: - Extract assertACLErrorContains helper with progressive fallbacks: direct match, mapped equivalent, key-part extraction, then t.Errorf - Convert HeadscaleDiffers from t.Logf to t.Skipf for visibility - Add aclErrorMessageMap for known wording differences (empty for now) All 16 ACL error golden files pass with the strict handler. Updates #3157
This commit is contained in:
@@ -25,7 +25,6 @@ import (
|
|||||||
"github.com/google/go-cmp/cmp/cmpopts"
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
"github.com/juanfont/headscale/hscontrol/policy/policyutil"
|
"github.com/juanfont/headscale/hscontrol/policy/policyutil"
|
||||||
"github.com/juanfont/headscale/hscontrol/types"
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/tailscale/hujson"
|
"github.com/tailscale/hujson"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -301,6 +300,13 @@ func TestACLCompat(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// aclErrorMessageMap maps Tailscale SaaS error substrings to headscale
|
||||||
|
// equivalents. Populated as mismatches are discovered.
|
||||||
|
var aclErrorMessageMap = map[string]string{
|
||||||
|
// Add known wording differences here as they are discovered.
|
||||||
|
// Example: "tag not found" -> "undefined tag",
|
||||||
|
}
|
||||||
|
|
||||||
// testACLError verifies that an invalid policy produces the expected error.
|
// testACLError verifies that an invalid policy produces the expected error.
|
||||||
func testACLError(t *testing.T, tf aclTestFile) {
|
func testACLError(t *testing.T, tf aclTestFile) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
@@ -309,16 +315,12 @@ func testACLError(t *testing.T, tf aclTestFile) {
|
|||||||
|
|
||||||
pol, err := unmarshalPolicy(policyJSON)
|
pol, err := unmarshalPolicy(policyJSON)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Parse-time error — valid for some error tests
|
// Parse-time error.
|
||||||
if tf.Input.APIResponseBody != nil {
|
if tf.Input.APIResponseBody != nil {
|
||||||
wantMsg := tf.Input.APIResponseBody.Message
|
wantMsg := tf.Input.APIResponseBody.Message
|
||||||
if wantMsg != "" {
|
if wantMsg != "" {
|
||||||
assert.Contains(
|
assertACLErrorContains(
|
||||||
t,
|
t, err, wantMsg, tf.TestID,
|
||||||
err.Error(),
|
|
||||||
wantMsg,
|
|
||||||
"%s: error message should contain expected substring",
|
|
||||||
tf.TestID,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -331,46 +333,19 @@ func testACLError(t *testing.T, tf aclTestFile) {
|
|||||||
if tf.Input.APIResponseBody != nil {
|
if tf.Input.APIResponseBody != nil {
|
||||||
wantMsg := tf.Input.APIResponseBody.Message
|
wantMsg := tf.Input.APIResponseBody.Message
|
||||||
if wantMsg != "" {
|
if wantMsg != "" {
|
||||||
// Allow partial match — headscale error messages differ
|
assertACLErrorContains(
|
||||||
// from Tailscale's
|
t, err, wantMsg, tf.TestID,
|
||||||
errStr := err.Error()
|
)
|
||||||
if !strings.Contains(errStr, wantMsg) {
|
|
||||||
// Try matching key parts
|
|
||||||
matched := false
|
|
||||||
|
|
||||||
for _, part := range []string{
|
|
||||||
"autogroup:self",
|
|
||||||
"not valid on the src",
|
|
||||||
"port range",
|
|
||||||
"tag not found",
|
|
||||||
"undefined",
|
|
||||||
} {
|
|
||||||
if strings.Contains(wantMsg, part) &&
|
|
||||||
strings.Contains(errStr, part) {
|
|
||||||
matched = true
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !matched {
|
|
||||||
t.Logf(
|
|
||||||
"%s: error message difference\n want (tailscale): %q\n got (headscale): %q",
|
|
||||||
tf.TestID,
|
|
||||||
wantMsg,
|
|
||||||
errStr,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// For headscale_differs tests, headscale may accept what Tailscale rejects
|
// For headscale_differs tests, headscale may accept what
|
||||||
|
// Tailscale rejects. Log as skip so it appears in output.
|
||||||
if tf.HeadscaleDiffers {
|
if tf.HeadscaleDiffers {
|
||||||
t.Logf(
|
t.Skipf(
|
||||||
"%s: headscale accepts this policy (Tailscale rejects it)",
|
"%s: headscale accepts this policy (Tailscale rejects it)",
|
||||||
tf.TestID,
|
tf.TestID,
|
||||||
)
|
)
|
||||||
@@ -384,6 +359,62 @@ func testACLError(t *testing.T, tf aclTestFile) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// assertACLErrorContains checks that an error message matches the
|
||||||
|
// expected Tailscale SaaS message, using progressive fallbacks:
|
||||||
|
// 1. Direct substring match
|
||||||
|
// 2. Mapped equivalent from aclErrorMessageMap
|
||||||
|
// 3. Key-part extraction (tags, autogroups, port, undefined)
|
||||||
|
// 4. t.Errorf on no match (strict)
|
||||||
|
func assertACLErrorContains(
|
||||||
|
t *testing.T,
|
||||||
|
err error,
|
||||||
|
wantMsg string,
|
||||||
|
testID string,
|
||||||
|
) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
errStr := err.Error()
|
||||||
|
|
||||||
|
// 1. Direct substring match.
|
||||||
|
if strings.Contains(errStr, wantMsg) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Mapped equivalent.
|
||||||
|
for tsKey, hsKey := range aclErrorMessageMap {
|
||||||
|
if strings.Contains(wantMsg, tsKey) &&
|
||||||
|
strings.Contains(errStr, hsKey) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Key-part extraction.
|
||||||
|
for _, part := range []string{
|
||||||
|
"autogroup:self",
|
||||||
|
"not valid on the src",
|
||||||
|
"port range",
|
||||||
|
"tag not found",
|
||||||
|
"tag:",
|
||||||
|
"undefined",
|
||||||
|
"capability",
|
||||||
|
} {
|
||||||
|
if strings.Contains(wantMsg, part) &&
|
||||||
|
strings.Contains(errStr, part) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. No match — strict failure.
|
||||||
|
t.Errorf(
|
||||||
|
"%s: error message mismatch\n"+
|
||||||
|
" want (tailscale): %q\n"+
|
||||||
|
" got (headscale): %q",
|
||||||
|
testID,
|
||||||
|
wantMsg,
|
||||||
|
errStr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// testACLSuccess verifies that a valid policy produces the expected
|
// testACLSuccess verifies that a valid policy produces the expected
|
||||||
// packet filter rules for each node.
|
// packet filter rules for each node.
|
||||||
func testACLSuccess(
|
func testACLSuccess(
|
||||||
|
|||||||
Reference in New Issue
Block a user