mirror of
https://github.com/juanfont/headscale.git
synced 2025-07-27 13:48:02 +02:00
policy: ensure group and tags can only have ascii chars and dash
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
parent
27a518a2fa
commit
4d3483ed3a
@ -205,10 +205,14 @@ func (u Username) Resolve(_ *Policy, users types.Users, nodes types.Nodes) (*net
|
||||
type Group string
|
||||
|
||||
func (g Group) Validate() error {
|
||||
if isGroup(string(g)) {
|
||||
return nil
|
||||
if !isGroup(string(g)) {
|
||||
return fmt.Errorf(`Group has to start with "group:", got: %q`, g)
|
||||
}
|
||||
return fmt.Errorf(`Group has to start with "group:", got: %q`, g)
|
||||
|
||||
// Group name is everything after "group:"
|
||||
groupName := string(g)[len("group:"):]
|
||||
|
||||
return validateNameFormat(groupName, "Group", g)
|
||||
}
|
||||
|
||||
func (g *Group) UnmarshalJSON(b []byte) error {
|
||||
@ -266,10 +270,14 @@ func (g Group) Resolve(p *Policy, users types.Users, nodes types.Nodes) (*netipx
|
||||
type Tag string
|
||||
|
||||
func (t Tag) Validate() error {
|
||||
if isTag(string(t)) {
|
||||
return nil
|
||||
if !isTag(string(t)) {
|
||||
return fmt.Errorf(`tag has to start with "tag:", got: %q`, t)
|
||||
}
|
||||
return fmt.Errorf(`tag has to start with "tag:", got: %q`, t)
|
||||
|
||||
// Tag name is everything after "tag:"
|
||||
tagName := string(t)[len("tag:"):]
|
||||
|
||||
return validateNameFormat(tagName, "Tag", t)
|
||||
}
|
||||
|
||||
func (t *Tag) UnmarshalJSON(b []byte) error {
|
||||
@ -654,6 +662,36 @@ func isTag(str string) bool {
|
||||
return strings.HasPrefix(str, "tag:")
|
||||
}
|
||||
|
||||
// validateNameFormat checks if a name follows the required format:
|
||||
// - Must start with an ASCII letter (a-z, A-Z)
|
||||
// - Can only contain ASCII letters, numbers, or dashes
|
||||
func validateNameFormat(name string, typeLabel string, original interface{}) error {
|
||||
// Check if empty
|
||||
if len(name) == 0 {
|
||||
return fmt.Errorf(`%s names cannot be empty, got: %q`, typeLabel, original)
|
||||
}
|
||||
|
||||
// Check if first character is an ASCII letter
|
||||
firstChar := name[0]
|
||||
if !((firstChar >= 'a' && firstChar <= 'z') || (firstChar >= 'A' && firstChar <= 'Z')) {
|
||||
return fmt.Errorf(`%s names must start with a letter, got: %q`, typeLabel, original)
|
||||
}
|
||||
|
||||
// Check if all characters are ASCII letters, numbers, or dashes
|
||||
for i := 0; i < len(name); i++ {
|
||||
char := name[i]
|
||||
isAsciiLetter := (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z')
|
||||
isDigit := char >= '0' && char <= '9'
|
||||
isDash := char == '-'
|
||||
|
||||
if !isAsciiLetter && !isDigit && !isDash {
|
||||
return fmt.Errorf(`%s names can only contain ASCII letters, numbers, or dashes, got: %q`, typeLabel, original)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isAutoGroup(str string) bool {
|
||||
return strings.HasPrefix(str, "autogroup:")
|
||||
}
|
||||
@ -1077,6 +1115,39 @@ func (to TagOwners) MarshalJSON() ([]byte, error) {
|
||||
// TagOwners are a map of Tag to a list of the UserEntities that own the tag.
|
||||
type TagOwners map[Tag]Owners
|
||||
|
||||
// UnmarshalJSON overrides the default JSON unmarshalling for TagOwners to ensure
|
||||
// that each tag name is validated using the isTag function and character validation rules.
|
||||
// This ensures that all tag names conform to the expected format and character rules.
|
||||
func (to *TagOwners) UnmarshalJSON(b []byte) error {
|
||||
var rawTagOwners map[string][]string
|
||||
if err := json.Unmarshal(b, &rawTagOwners); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*to = make(TagOwners)
|
||||
for key, value := range rawTagOwners {
|
||||
tag := Tag(key)
|
||||
if err := tag.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var owners Owners
|
||||
|
||||
for _, o := range value {
|
||||
owner, err := parseOwner(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
owners = append(owners, owner)
|
||||
}
|
||||
|
||||
(*to)[tag] = owners
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (to TagOwners) Contains(tagOwner *Tag) error {
|
||||
if tagOwner == nil {
|
||||
return nil
|
||||
|
@ -389,6 +389,150 @@ func TestUnmarshalPolicy(t *testing.T) {
|
||||
// wantErr: `Username has to contain @, got: "group:inner"`,
|
||||
wantErr: `Nested groups are not allowed, found "group:inner" inside "group:example"`,
|
||||
},
|
||||
{
|
||||
name: "invalid-group-name-special-chars",
|
||||
input: `
|
||||
{
|
||||
"groups": {
|
||||
"group:example@invalid": [
|
||||
"valid@example.com",
|
||||
],
|
||||
},
|
||||
}
|
||||
`,
|
||||
wantErr: `Group names can only contain ASCII letters, numbers, or dashes, got: "group:example@invalid"`,
|
||||
},
|
||||
{
|
||||
name: "invalid-group-name-starting-with-number",
|
||||
input: `
|
||||
{
|
||||
"groups": {
|
||||
"group:123example": [
|
||||
"valid@example.com",
|
||||
],
|
||||
},
|
||||
}
|
||||
`,
|
||||
wantErr: `Group names must start with a letter, got: "group:123example"`,
|
||||
},
|
||||
{
|
||||
name: "invalid-group-name-scandinavian-characters",
|
||||
input: `
|
||||
{
|
||||
"groups": {
|
||||
"group:æøå-example": [
|
||||
"valid@example.com",
|
||||
],
|
||||
},
|
||||
}
|
||||
`,
|
||||
wantErr: `Group names must start with a letter, got: "group:æøå-example"`,
|
||||
},
|
||||
{
|
||||
name: "invalid-group-name-cyrillic-characters",
|
||||
input: `
|
||||
{
|
||||
"groups": {
|
||||
"group:группа": [
|
||||
"valid@example.com",
|
||||
],
|
||||
},
|
||||
}
|
||||
`,
|
||||
wantErr: `Group names must start with a letter, got: "group:группа"`,
|
||||
},
|
||||
{
|
||||
name: "invalid-group-name-emoji",
|
||||
input: `
|
||||
{
|
||||
"groups": {
|
||||
"group:dev-😊": [
|
||||
"valid@example.com",
|
||||
],
|
||||
},
|
||||
}
|
||||
`,
|
||||
wantErr: `Group names can only contain ASCII letters, numbers, or dashes, got: "group:dev-😊"`,
|
||||
},
|
||||
{
|
||||
name: "invalid-group-name-other-special-chars",
|
||||
input: `
|
||||
{
|
||||
"groups": {
|
||||
"group:dev_team": [
|
||||
"valid@example.com",
|
||||
],
|
||||
},
|
||||
}
|
||||
`,
|
||||
wantErr: `Group names can only contain ASCII letters, numbers, or dashes, got: "group:dev_team"`,
|
||||
},
|
||||
{
|
||||
name: "invalid-tag-name-special-chars",
|
||||
input: `
|
||||
{
|
||||
"tagOwners": {
|
||||
"tag:test@invalid": ["valid@example.com"],
|
||||
},
|
||||
}
|
||||
`,
|
||||
wantErr: `Tag names can only contain ASCII letters, numbers, or dashes, got: "tag:test@invalid"`,
|
||||
},
|
||||
{
|
||||
name: "invalid-tag-name-starting-with-number",
|
||||
input: `
|
||||
{
|
||||
"tagOwners": {
|
||||
"tag:123test": ["valid@example.com"],
|
||||
},
|
||||
}
|
||||
`,
|
||||
wantErr: `Tag names must start with a letter, got: "tag:123test"`,
|
||||
},
|
||||
{
|
||||
name: "invalid-tag-name-scandinavian-characters",
|
||||
input: `
|
||||
{
|
||||
"tagOwners": {
|
||||
"tag:æøå-test": ["valid@example.com"],
|
||||
},
|
||||
}
|
||||
`,
|
||||
wantErr: `Tag names must start with a letter, got: "tag:æøå-test"`,
|
||||
},
|
||||
{
|
||||
name: "invalid-tag-name-cyrillic-characters",
|
||||
input: `
|
||||
{
|
||||
"tagOwners": {
|
||||
"tag:тест": ["valid@example.com"],
|
||||
},
|
||||
}
|
||||
`,
|
||||
wantErr: `Tag names must start with a letter, got: "tag:тест"`,
|
||||
},
|
||||
{
|
||||
name: "invalid-tag-name-emoji",
|
||||
input: `
|
||||
{
|
||||
"tagOwners": {
|
||||
"tag:test-😊": ["valid@example.com"],
|
||||
},
|
||||
}
|
||||
`,
|
||||
wantErr: `Tag names can only contain ASCII letters, numbers, or dashes, got: "tag:test-😊"`,
|
||||
},
|
||||
{
|
||||
name: "invalid-tag-name-other-special-chars",
|
||||
input: `
|
||||
{
|
||||
"tagOwners": {
|
||||
"tag:test_underscore": ["valid@example.com"],
|
||||
},
|
||||
}
|
||||
`,
|
||||
wantErr: `Tag names can only contain ASCII letters, numbers, or dashes, got: "tag:test_underscore"`,
|
||||
},
|
||||
{
|
||||
name: "invalid-addr",
|
||||
input: `
|
||||
|
Loading…
Reference in New Issue
Block a user