mirror of
https://github.com/juanfont/headscale.git
synced 2026-10-25 15:56:49 +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:
1 parent
76ba2de85a
commit
9f362d5be9
3 files changed
+57
-14
No files matched your search
@@ -88,19 +88,6 @@ var sshSkipReasons = map[string]string{
|
|||||||
"ssh-b5": "user:*@passkey wildcard not supported in headscale",
|
"ssh-b5": "user:*@passkey wildcard not supported in headscale",
|
||||||
"ssh-d10": "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)
|
// TAG_OWNER_CYCLE_TOLERATED (2 tests)
|
||||||
//
|
//
|
||||||
// SaaS accepts tagOwners with circular references (tag:a -> tag:b,
|
// SaaS accepts tagOwners with circular references (tag:a -> tag:b,
|
||||||
|
|||||||
@@ -1063,10 +1063,17 @@ func parseAlias(vs string) (Alias, error) {
|
|||||||
// AliasEnc is used to deserialize a Alias.
|
// AliasEnc is used to deserialize a Alias.
|
||||||
type AliasEnc struct{ 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 {
|
func (ve *AliasEnc) UnmarshalJSON(b []byte) error {
|
||||||
ptr, err := unmarshalPointer(
|
ptr, err := unmarshalPointer(
|
||||||
b,
|
b,
|
||||||
parseAlias,
|
func(s string) (Alias, error) {
|
||||||
|
return parseAlias(strings.TrimSpace(s))
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
// TestSSHCheckPeriodInvalidDuration verifies the SaaS body for the
|
||||||
// malformed-duration case (`time: invalid duration "abc"`).
|
// malformed-duration case (`time: invalid duration "abc"`).
|
||||||
func TestSSHCheckPeriodInvalidDuration(t *testing.T) {
|
func TestSSHCheckPeriodInvalidDuration(t *testing.T) {
|
||||||
|
|||||||
Reference in new issue
Block a user