feat: validate server tags and add the model field
This commit is contained in:
@@ -44,24 +44,25 @@ type Inventory struct {
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
ID bson.ObjectID `bson:"_id,omitempty" json:"_id,omitempty"`
|
||||
InstanceID string `bson:"instance_id" json:"instance_id"`
|
||||
ServerID string `bson:"server_id" json:"server_id"`
|
||||
Hostname string `bson:"hostname" json:"hostname"`
|
||||
IPAddress string `bson:"ip_address" json:"ip_address"`
|
||||
OSInfo string `bson:"os_info" json:"os_info"`
|
||||
OSType string `bson:"os_type,omitempty" json:"os_type,omitempty"`
|
||||
ConsoleProtocols []string `bson:"console_protocols,omitempty" json:"console_protocols,omitempty"`
|
||||
SSHPort int `bson:"ssh_port,omitempty" json:"ssh_port,omitempty"`
|
||||
RDPPort int `bson:"rdp_port,omitempty" json:"rdp_port,omitempty"`
|
||||
PreRegToken string `bson:"pre_reg_token,omitempty" json:"pre_reg_token,omitempty"`
|
||||
PreRegExpires *time.Time `bson:"pre_reg_expires,omitempty" json:"pre_reg_expires,omitempty"`
|
||||
AgentTokenHash string `bson:"agent_token_hash,omitempty" json:"-"`
|
||||
Status string `bson:"status" json:"status"`
|
||||
AgentVersion string `bson:"agent_version,omitempty" json:"agent_version,omitempty"`
|
||||
LastSeen *time.Time `bson:"last_seen,omitempty" json:"last_seen,omitempty"`
|
||||
AvailableUpdates []PackageUpdate `bson:"available_updates,omitempty" json:"available_updates,omitempty"`
|
||||
UpdatesCheckedAt *time.Time `bson:"updates_checked_at,omitempty" json:"updates_checked_at,omitempty"`
|
||||
Inventory *Inventory `bson:"inventory,omitempty" json:"inventory,omitempty"`
|
||||
CreatedAt time.Time `bson:"created_at" json:"created_at"`
|
||||
ID bson.ObjectID `bson:"_id,omitempty" json:"_id,omitempty"`
|
||||
InstanceID string `bson:"instance_id" json:"instance_id"`
|
||||
ServerID string `bson:"server_id" json:"server_id"`
|
||||
Hostname string `bson:"hostname" json:"hostname"`
|
||||
IPAddress string `bson:"ip_address" json:"ip_address"`
|
||||
OSInfo string `bson:"os_info" json:"os_info"`
|
||||
OSType string `bson:"os_type,omitempty" json:"os_type,omitempty"`
|
||||
ConsoleProtocols []string `bson:"console_protocols,omitempty" json:"console_protocols,omitempty"`
|
||||
SSHPort int `bson:"ssh_port,omitempty" json:"ssh_port,omitempty"`
|
||||
RDPPort int `bson:"rdp_port,omitempty" json:"rdp_port,omitempty"`
|
||||
PreRegToken string `bson:"pre_reg_token,omitempty" json:"pre_reg_token,omitempty"`
|
||||
PreRegExpires *time.Time `bson:"pre_reg_expires,omitempty" json:"pre_reg_expires,omitempty"`
|
||||
AgentTokenHash string `bson:"agent_token_hash,omitempty" json:"-"`
|
||||
Status string `bson:"status" json:"status"`
|
||||
AgentVersion string `bson:"agent_version,omitempty" json:"agent_version,omitempty"`
|
||||
LastSeen *time.Time `bson:"last_seen,omitempty" json:"last_seen,omitempty"`
|
||||
AvailableUpdates []PackageUpdate `bson:"available_updates,omitempty" json:"available_updates,omitempty"`
|
||||
UpdatesCheckedAt *time.Time `bson:"updates_checked_at,omitempty" json:"updates_checked_at,omitempty"`
|
||||
Inventory *Inventory `bson:"inventory,omitempty" json:"inventory,omitempty"`
|
||||
Tags map[string]string `bson:"tags,omitempty" json:"tags,omitempty"`
|
||||
CreatedAt time.Time `bson:"created_at" json:"created_at"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ErrInvalidTag is returned for any tag the rules below reject. Handlers map
|
||||
// it to 400 — a malformed tag is the caller's mistake, not a server fault.
|
||||
var ErrInvalidTag = errors.New("invalid tag")
|
||||
|
||||
const (
|
||||
maxTagKeyLen = 32
|
||||
maxTagValueLen = 64
|
||||
maxTagsPerHost = 20
|
||||
// Reserved for tags the agent may derive from inventory later. Refusing
|
||||
// it now means a user tag written today can never collide with a system
|
||||
// tag invented tomorrow.
|
||||
sysTagPrefix = "sys:"
|
||||
)
|
||||
|
||||
func validTagRunes(s string) bool {
|
||||
for _, r := range s {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z':
|
||||
case r >= '0' && r <= '9':
|
||||
case r == '-' || r == '_':
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ValidateTags enforces the shape of a whole tag map. It lives in the service
|
||||
// layer rather than a handler so that every write path — the tags endpoint,
|
||||
// server create, anything added later — agrees on what a valid tag is.
|
||||
func ValidateTags(tags map[string]string) error {
|
||||
if len(tags) > maxTagsPerHost {
|
||||
return fmt.Errorf("%w: at most %d tags per server", ErrInvalidTag, maxTagsPerHost)
|
||||
}
|
||||
for k, v := range tags {
|
||||
if strings.HasPrefix(k, sysTagPrefix) {
|
||||
return fmt.Errorf("%w: keys beginning %q are reserved", ErrInvalidTag, sysTagPrefix)
|
||||
}
|
||||
if k == "" || len(k) > maxTagKeyLen || !validTagRunes(k) {
|
||||
return fmt.Errorf("%w: key %q must be 1-%d chars of a-z, 0-9, - or _", ErrInvalidTag, k, maxTagKeyLen)
|
||||
}
|
||||
if v == "" || len(v) > maxTagValueLen || !validTagRunes(v) {
|
||||
return fmt.Errorf("%w: value for %q must be 1-%d chars of a-z, 0-9, - or _", ErrInvalidTag, k, maxTagValueLen)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user