mirror of
https://github.com/juanfont/headscale.git
synced 2025-08-01 13:46:49 +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
|
type Group string
|
||||||
|
|
||||||
func (g Group) Validate() error {
|
func (g Group) Validate() error {
|
||||||
if isGroup(string(g)) {
|
if !isGroup(string(g)) {
|
||||||
return nil
|
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 {
|
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
|
type Tag string
|
||||||
|
|
||||||
func (t Tag) Validate() error {
|
func (t Tag) Validate() error {
|
||||||
if isTag(string(t)) {
|
if !isTag(string(t)) {
|
||||||
return nil
|
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 {
|
func (t *Tag) UnmarshalJSON(b []byte) error {
|
||||||
@ -654,6 +662,36 @@ func isTag(str string) bool {
|
|||||||
return strings.HasPrefix(str, "tag:")
|
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 {
|
func isAutoGroup(str string) bool {
|
||||||
return strings.HasPrefix(str, "autogroup:")
|
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.
|
// TagOwners are a map of Tag to a list of the UserEntities that own the tag.
|
||||||
type TagOwners map[Tag]Owners
|
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 {
|
func (to TagOwners) Contains(tagOwner *Tag) error {
|
||||||
if tagOwner == nil {
|
if tagOwner == nil {
|
||||||
return nil
|
return nil
|
||||||
|
@ -389,6 +389,150 @@ func TestUnmarshalPolicy(t *testing.T) {
|
|||||||
// wantErr: `Username has to contain @, got: "group:inner"`,
|
// wantErr: `Username has to contain @, got: "group:inner"`,
|
||||||
wantErr: `Nested groups are not allowed, found "group:inner" inside "group:example"`,
|
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",
|
name: "invalid-addr",
|
||||||
input: `
|
input: `
|
||||||
|
Loading…
Reference in New Issue
Block a user