mirror of
https://github.com/juanfont/headscale.git
synced 2026-07-07 16:40:21 +09:00
policy/v2: trim whitespace in SSH src and dst aliases
SaaS trims surrounding whitespace from src/dst entries before dispatching to the alias lookup, so `"tag:server "` resolves to the same tag and `" odin@example.com"` resolves to the same user. headscale was treating the untrimmed strings as literals, which either failed the tag/user lookup or dropped the affected node from every rule referencing it. Trim inside AliasEnc.UnmarshalJSON so ACL src/dst and SSH src/dst all benefit.
This commit is contained in:
@@ -88,19 +88,6 @@ var sshSkipReasons = map[string]string{
|
||||
"ssh-b5": "user:*@passkey wildcard not supported in headscale",
|
||||
"ssh-d10": "user:*@passkey wildcard not supported in headscale",
|
||||
|
||||
// WHITESPACE_TRIM_IN_SRC_DST (2 tests)
|
||||
//
|
||||
// SaaS strips surrounding whitespace from SSH rule src/dst entries
|
||||
// before lookup ("tag:server " resolves to "tag:server",
|
||||
// " user@host" resolves to "user@host"). headscale treats the
|
||||
// untrimmed string as a literal identifier and either fails the
|
||||
// tag/user lookup or produces no rule for the affected node. The
|
||||
// fix lives in policy/v2 alias resolution — strings.TrimSpace at
|
||||
// the same point where SaaS does, before dispatching to the alias
|
||||
// type registry.
|
||||
"ssh-dst-trailing-whitespace": "src/dst whitespace trim mismatch: SaaS trims, headscale doesn't",
|
||||
"ssh-src-leading-whitespace": "src/dst whitespace trim mismatch: SaaS trims, headscale doesn't",
|
||||
|
||||
// TAG_OWNER_CYCLE_TOLERATED (2 tests)
|
||||
//
|
||||
// SaaS accepts tagOwners with circular references (tag:a -> tag:b,
|
||||
|
||||
@@ -1043,10 +1043,17 @@ func parseAlias(vs string) (Alias, error) {
|
||||
// AliasEnc is used to deserialize a Alias.
|
||||
type AliasEnc struct{ Alias }
|
||||
|
||||
// UnmarshalJSON trims surrounding whitespace from each alias string
|
||||
// before dispatching so that `"tag:server "` or `" odin@example.com"`
|
||||
// resolves to the same tag or user SaaS would resolve. SaaS trims
|
||||
// before lookup; a literal-match policy here would drop the affected
|
||||
// node from every rule referencing it.
|
||||
func (ve *AliasEnc) UnmarshalJSON(b []byte) error {
|
||||
ptr, err := unmarshalPointer(
|
||||
b,
|
||||
parseAlias,
|
||||
func(s string) (Alias, error) {
|
||||
return parseAlias(strings.TrimSpace(s))
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -4787,6 +4787,55 @@ func TestSSHUserTrimEndToEnd(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestAliasEncUnmarshalTrim verifies that src/dst entries get
|
||||
// trimmed before alias dispatch so `"tag:server "` resolves to the
|
||||
// same Tag alias SaaS uses and `" odin@example.com"` resolves to the
|
||||
// same Username alias. Covers tag, group, user, and autogroup entries
|
||||
// on both the leading- and trailing-whitespace edges.
|
||||
func TestAliasEncUnmarshalTrim(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want Alias
|
||||
}{
|
||||
{
|
||||
name: "tag trailing whitespace",
|
||||
input: `"tag:server "`,
|
||||
want: new(Tag("tag:server")),
|
||||
},
|
||||
{
|
||||
name: "tag leading whitespace",
|
||||
input: `" tag:server"`,
|
||||
want: new(Tag("tag:server")),
|
||||
},
|
||||
{
|
||||
name: "group leading whitespace",
|
||||
input: `" group:admins"`,
|
||||
want: new(Group("group:admins")),
|
||||
},
|
||||
{
|
||||
name: "user trailing whitespace",
|
||||
input: `"odin@example.com "`,
|
||||
want: new(Username("odin@example.com")),
|
||||
},
|
||||
{
|
||||
name: "autogroup trailing whitespace",
|
||||
input: `"autogroup:member "`,
|
||||
want: new(AutoGroup("autogroup:member")),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var a AliasEnc
|
||||
|
||||
err := json.Unmarshal([]byte(tt.input), &a)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.want, a.Alias)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSSHCheckPeriodInvalidDuration verifies the SaaS body for the
|
||||
// malformed-duration case (`time: invalid duration "abc"`).
|
||||
func TestSSHCheckPeriodInvalidDuration(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user