Commit 43b2c65f authored by ale's avatar ale

Make more validation parameters configurable

parent 352baf08
Pipeline #1581 passed with stages
in 1 minute and 29 seconds
...@@ -245,8 +245,8 @@ func createFakeBackend() *fakeBackend { ...@@ -245,8 +245,8 @@ func createFakeBackend() *fakeBackend {
func testConfig() *Config { func testConfig() *Config {
var c Config var c Config
c.ForbiddenUsernames = []string{"root"} c.Validation.ForbiddenUsernames = []string{"root"}
c.AvailableDomains = map[string][]string{ c.Validation.AvailableDomains = map[string][]string{
ResourceTypeEmail: []string{"example.com"}, ResourceTypeEmail: []string{"example.com"},
ResourceTypeMailingList: []string{"example.com"}, ResourceTypeMailingList: []string{"example.com"},
} }
......
package accountserver package accountserver
import ( import (
"errors"
"io/ioutil" "io/ioutil"
"git.autistici.org/ai3/go-common/clientutil" "git.autistici.org/ai3/go-common/clientutil"
...@@ -9,12 +10,7 @@ import ( ...@@ -9,12 +10,7 @@ import (
// Config holds the configuration for the AccountService. // Config holds the configuration for the AccountService.
type Config struct { type Config struct {
ForbiddenUsernames []string `yaml:"forbidden_usernames"` Validation ValidationConfig `yaml:",inline"`
ForbiddenUsernamesFile string `yaml:"forbidden_usernames_file"`
ForbiddenPasswords []string `yaml:"forbidden_passwords"`
ForbiddenPasswordsFile string `yaml:"forbidden_passwords_file"`
AvailableDomains map[string][]string `yaml:"available_domains"`
WebsiteRootDir string `yaml:"website_root_dir"`
Shards struct { Shards struct {
Available map[string][]string `yaml:"available"` Available map[string][]string `yaml:"available"`
...@@ -34,9 +30,16 @@ type Config struct { ...@@ -34,9 +30,16 @@ type Config struct {
EnableOpportunisticEncryption bool `yaml:"enable_opportunistic_encryption"` EnableOpportunisticEncryption bool `yaml:"enable_opportunistic_encryption"`
} }
func (c *Config) compile() error {
if len(c.Shards.Allowed) == 0 {
return errors.New("no allowed shards")
}
return c.Validation.compile()
}
func (c *Config) domainBackend() domainBackend { func (c *Config) domainBackend() domainBackend {
b := &staticDomainBackend{sets: make(map[string]stringSet)} b := &staticDomainBackend{sets: make(map[string]stringSet)}
for kind, list := range c.AvailableDomains { for kind, list := range c.Validation.AvailableDomains {
b.sets[kind] = newStringSetFromList(list) b.sets[kind] = newStringSetFromList(list)
} }
return b return b
...@@ -58,30 +61,18 @@ func (c *Config) shardBackend() shardBackend { ...@@ -58,30 +61,18 @@ func (c *Config) shardBackend() shardBackend {
} }
func (c *Config) validationContext(be Backend) (*validationContext, error) { func (c *Config) validationContext(be Backend) (*validationContext, error) {
fu, err := newStringSetFromFileOrList(c.ForbiddenUsernames, c.ForbiddenUsernamesFile)
if err != nil {
return nil, err
}
fp, err := newStringSetFromFileOrList(c.ForbiddenPasswords, c.ForbiddenPasswordsFile)
if err != nil {
return nil, err
}
return &validationContext{ return &validationContext{
forbiddenUsernames: fu, config: &c.Validation,
forbiddenPasswords: fp, domains: c.domainBackend(),
minPasswordLength: 6, shards: c.shardBackend(),
maxPasswordLength: 128, backend: be,
webroot: c.WebsiteRootDir,
domains: c.domainBackend(),
shards: c.shardBackend(),
backend: be,
}, nil }, nil
} }
func (c *Config) templateContext() *templateContext { func (c *Config) templateContext() *templateContext {
return &templateContext{ return &templateContext{
shards: c.shardBackend(), shards: c.shardBackend(),
webroot: c.WebsiteRootDir, webroot: c.Validation.WebsiteRootDir,
} }
} }
......
...@@ -124,11 +124,12 @@ func startServiceWithConfig(t testing.TB, svcConfig as.Config) (func(), as.Backe ...@@ -124,11 +124,12 @@ func startServiceWithConfig(t testing.TB, svcConfig as.Config) (func(), as.Backe
svcConfig.SSO.Domain = testSSODomain svcConfig.SSO.Domain = testSSODomain
svcConfig.SSO.Service = testSSOService svcConfig.SSO.Service = testSSOService
svcConfig.SSO.AdminGroup = testAdminGroup svcConfig.SSO.AdminGroup = testAdminGroup
svcConfig.ForbiddenUsernames = []string{"forbidden"} svcConfig.Validation.ForbiddenUsernames = []string{"forbidden"}
svcConfig.AvailableDomains = map[string][]string{ svcConfig.Validation.AvailableDomains = map[string][]string{
as.ResourceTypeEmail: []string{"example.com"}, as.ResourceTypeEmail: []string{"example.com"},
as.ResourceTypeMailingList: []string{"example.com"}, as.ResourceTypeMailingList: []string{"example.com"},
} }
svcConfig.Validation.WebsiteRootDir = "/home/users/investici.org"
shards := []string{"host1", "host2", "host3"} shards := []string{"host1", "host2", "host3"}
svcConfig.Shards.Available = map[string][]string{ svcConfig.Shards.Available = map[string][]string{
as.ResourceTypeEmail: shards, as.ResourceTypeEmail: shards,
...@@ -139,7 +140,6 @@ func startServiceWithConfig(t testing.TB, svcConfig as.Config) (func(), as.Backe ...@@ -139,7 +140,6 @@ func startServiceWithConfig(t testing.TB, svcConfig as.Config) (func(), as.Backe
as.ResourceTypeDatabase: shards, as.ResourceTypeDatabase: shards,
} }
svcConfig.Shards.Allowed = svcConfig.Shards.Available svcConfig.Shards.Allowed = svcConfig.Shards.Available
svcConfig.WebsiteRootDir = "/home/users/investici.org"
service, err := as.NewAccountService(be, &svcConfig) service, err := as.NewAccountService(be, &svcConfig)
if err != nil { if err != nil {
......
...@@ -2,6 +2,7 @@ package accountserver ...@@ -2,6 +2,7 @@ package accountserver
import ( import (
"context" "context"
"fmt"
"log" "log"
"time" "time"
...@@ -103,10 +104,14 @@ func NewAccountService(backend Backend, config *Config) (*AccountService, error) ...@@ -103,10 +104,14 @@ func NewAccountService(backend Backend, config *Config) (*AccountService, error)
} }
func newAccountServiceWithSSO(backend Backend, config *Config, ssoValidator sso.Validator) (*AccountService, error) { func newAccountServiceWithSSO(backend Backend, config *Config, ssoValidator sso.Validator) (*AccountService, error) {
if err := config.compile(); err != nil {
return nil, fmt.Errorf("configuration error: %v", err)
}
s := &AccountService{ s := &AccountService{
authService: newAuthService(config, ssoValidator), authService: newAuthService(config, ssoValidator),
audit: &syslogAuditLogger{}, audit: &syslogAuditLogger{},
backend: backend, backend: backend,
enableOpportunisticEncryption: config.EnableOpportunisticEncryption, enableOpportunisticEncryption: config.EnableOpportunisticEncryption,
} }
......
...@@ -27,22 +27,77 @@ type shardBackend interface { ...@@ -27,22 +27,77 @@ type shardBackend interface {
IsAllowedShard(context.Context, string, string) bool IsAllowedShard(context.Context, string, string) bool
} }
// ValidationConfig specifies a large number of validation-related
// configurable parameters.
type ValidationConfig struct {
ForbiddenUsernames []string `yaml:"forbidden_usernames"`
ForbiddenUsernamesFile string `yaml:"forbidden_usernames_file"`
ForbiddenPasswords []string `yaml:"forbidden_passwords"`
ForbiddenPasswordsFile string `yaml:"forbidden_passwords_file"`
AvailableDomains map[string][]string `yaml:"available_domains"`
WebsiteRootDir string `yaml:"website_root_dir"`
MinPasswordLen int `yaml:"min_password_len"`
MaxPasswordLen int `yaml:"max_password_len"`
MinUsernameLen int `yaml:"min_username_len"`
MaxUsernameLen int `yaml:"max_username_len"`
MinUID int `yaml:"min_backend_uid"`
MaxUID int `yaml:"max_backend_uid"`
forbiddenUsernames stringSet
forbiddenPasswords stringSet
}
const (
defaultMinPasswordLen = 8
defaultMaxPasswordLen = 128
defaultMinUsernameLen = 3
defaultMaxUsernameLen = 64
defaultMinUID = 1000
defaultMaxUID = 0
)
func (c *ValidationConfig) setDefaults() {
if c.MinPasswordLen == 0 {
c.MinPasswordLen = defaultMinPasswordLen
}
if c.MaxPasswordLen == 0 {
c.MaxPasswordLen = defaultMaxPasswordLen
}
if c.MinUsernameLen == 0 {
c.MinUsernameLen = defaultMinUsernameLen
}
if c.MaxUsernameLen == 0 {
c.MaxUsernameLen = defaultMaxUsernameLen
}
if c.MinUID == 0 {
c.MinUID = defaultMinUID
}
if c.MaxUID == 0 {
c.MaxUID = defaultMaxUID
}
}
func (c *ValidationConfig) compile() (err error) {
c.setDefaults()
c.forbiddenUsernames, err = newStringSetFromFileOrList(c.ForbiddenUsernames, c.ForbiddenUsernamesFile)
if err != nil {
return
}
c.forbiddenPasswords, err = newStringSetFromFileOrList(c.ForbiddenPasswords, c.ForbiddenPasswordsFile)
return
}
// The validationContext contains all configuration and backends that // The validationContext contains all configuration and backends that
// the various validation functions will need. Most methods on this // the various validation functions will need. Most methods on this
// object return functions themselves (ValidatorFunc or variations // object return functions themselves (ValidatorFunc or variations
// thereof) that can later be called multiple times at will and // thereof) that can later be called multiple times at will and
// combined with operators like 'allOf'. // combined with operators like 'allOf'.
type validationContext struct { type validationContext struct {
forbiddenUsernames stringSet config *ValidationConfig
forbiddenPasswords stringSet domains domainBackend
minPasswordLength int shards shardBackend
maxPasswordLength int backend Backend
minUID int
maxUID int
webroot string
domains domainBackend
shards shardBackend
backend Backend
} }
// A stringSet is just a list of strings with a quick membership test. // A stringSet is just a list of strings with a quick membership test.
...@@ -327,7 +382,12 @@ func (v *validationContext) isAvailableWebsite() ValidatorFunc { ...@@ -327,7 +382,12 @@ func (v *validationContext) isAvailableWebsite() ValidatorFunc {
func (v *validationContext) validHostedEmail() ValidatorFunc { func (v *validationContext) validHostedEmail() ValidatorFunc {
return allOf( return allOf(
validateUsernameAndDomain( validateUsernameAndDomain(
allOf(matchUsernameRx(), minLength(4), maxLength(64), notInSet(v.forbiddenUsernames)), allOf(
matchUsernameRx(),
minLength(v.config.MinUsernameLen),
maxLength(v.config.MaxUsernameLen),
notInSet(v.config.forbiddenUsernames),
),
allOf(v.isAllowedDomain(ResourceTypeEmail)), allOf(v.isAllowedDomain(ResourceTypeEmail)),
), ),
v.isAvailableEmailAddr(), v.isAvailableEmailAddr(),
...@@ -337,7 +397,12 @@ func (v *validationContext) validHostedEmail() ValidatorFunc { ...@@ -337,7 +397,12 @@ func (v *validationContext) validHostedEmail() ValidatorFunc {
func (v *validationContext) validHostedMailingList() ValidatorFunc { func (v *validationContext) validHostedMailingList() ValidatorFunc {
return allOf( return allOf(
validateUsernameAndDomain( validateUsernameAndDomain(
allOf(matchUsernameRx(), minLength(4), maxLength(64), notInSet(v.forbiddenUsernames)), allOf(
matchUsernameRx(),
minLength(v.config.MinUsernameLen),
maxLength(v.config.MaxUsernameLen),
notInSet(v.config.forbiddenUsernames),
),
allOf(v.isAllowedDomain(ResourceTypeMailingList)), allOf(v.isAllowedDomain(ResourceTypeMailingList)),
), ),
v.isAvailableEmailAddr(), v.isAvailableEmailAddr(),
...@@ -346,9 +411,9 @@ func (v *validationContext) validHostedMailingList() ValidatorFunc { ...@@ -346,9 +411,9 @@ func (v *validationContext) validHostedMailingList() ValidatorFunc {
func (v *validationContext) validPassword() ValidatorFunc { func (v *validationContext) validPassword() ValidatorFunc {
return allOf( return allOf(
minLength(v.minPasswordLength), minLength(v.config.MinPasswordLen),
maxLength(v.maxPasswordLength), maxLength(v.config.MaxPasswordLen),
notInSet(v.forbiddenPasswords), notInSet(v.config.forbiddenPasswords),
) )
} }
...@@ -473,8 +538,8 @@ func (v *validationContext) validateUID(uid int, user *User) error { ...@@ -473,8 +538,8 @@ func (v *validationContext) validateUID(uid int, user *User) error {
if uid == 0 { if uid == 0 {
return errors.New("uid is not set") return errors.New("uid is not set")
} }
if uid < v.minUID || (v.maxUID > 0 && uid > v.maxUID) { if uid < v.config.MinUID || (v.config.MaxUID > 0 && uid > v.config.MaxUID) {
return errors.New("uid outside of allowed range") return fmt.Errorf("uid %d outside of allowed range (%d-%d)", uid, v.config.MinUID, v.config.MaxUID)
} }
if user != nil && uid != user.UID { if user != nil && uid != user.UID {
return errors.New("uid of resource differs from uid of user") return errors.New("uid of resource differs from uid of user")
...@@ -489,7 +554,7 @@ func (v *validationContext) validateWebsiteCommon(r *Resource, user *User) error ...@@ -489,7 +554,7 @@ func (v *validationContext) validateWebsiteCommon(r *Resource, user *User) error
if r.Website.DocumentRoot == "" { if r.Website.DocumentRoot == "" {
return errors.New("empty document_root") return errors.New("empty document_root")
} }
if !isSubdir(v.webroot, r.Website.DocumentRoot) { if !isSubdir(v.config.WebsiteRootDir, r.Website.DocumentRoot) {
return errors.New("document root outside of web root") return errors.New("document root outside of web root")
} }
if !hasMatchingDAVAccount(user, r) { if !hasMatchingDAVAccount(user, r) {
...@@ -577,7 +642,7 @@ func (v *validationContext) validDAVResource() ResourceValidatorFunc { ...@@ -577,7 +642,7 @@ func (v *validationContext) validDAVResource() ResourceValidatorFunc {
if r.DAV == nil { if r.DAV == nil {
return errors.New("resource has no dav metadata") return errors.New("resource has no dav metadata")
} }
if !isSubdir(v.webroot, r.DAV.Homedir) { if !isSubdir(v.config.WebsiteRootDir, r.DAV.Homedir) {
return errors.New("homedir outside of web root") return errors.New("homedir outside of web root")
} }
......
...@@ -97,10 +97,12 @@ func (f *fakeCheckTX) HasAnyResource(_ context.Context, reqs []FindResourceReque ...@@ -97,10 +97,12 @@ func (f *fakeCheckTX) HasAnyResource(_ context.Context, reqs []FindResourceReque
return false, nil return false, nil
} }
func newTestValidationConfig(entries ...string) *validationContext { func newTestValidationContext(forbiddenUsernames ...string) *validationContext {
return &validationContext{ config := &ValidationConfig{
forbiddenUsernames: newStringSetFromList(entries), ForbiddenUsernames: forbiddenUsernames,
} }
config.compile()
return &validationContext{config: config}
} }
func newFakeDomainBackend(domains ...string) domainBackend { func newFakeDomainBackend(domains ...string) domainBackend {
...@@ -123,7 +125,7 @@ func TestValidator_HostedEmail(t *testing.T) { ...@@ -123,7 +125,7 @@ func TestValidator_HostedEmail(t *testing.T) {
{"forbidden@example.com", false}, {"forbidden@example.com", false},
{"existing@example.com", false}, {"existing@example.com", false},
} }
vc := newTestValidationConfig("forbidden") vc := newTestValidationContext("forbidden")
vc.backend = newFakeCheckBackend([]FindResourceRequest{ vc.backend = newFakeCheckBackend([]FindResourceRequest{
{Type: ResourceTypeEmail, Name: "existing@example.com"}, {Type: ResourceTypeEmail, Name: "existing@example.com"},
}) })
...@@ -141,7 +143,7 @@ func TestValidator_HostedMailingList(t *testing.T) { ...@@ -141,7 +143,7 @@ func TestValidator_HostedMailingList(t *testing.T) {
{"existing@domain1.com", false}, {"existing@domain1.com", false},
{"existing@domain2.com", false}, {"existing@domain2.com", false},
} }
vc := newTestValidationConfig("forbidden") vc := newTestValidationContext("forbidden")
vc.backend = newFakeCheckBackend([]FindResourceRequest{ vc.backend = newFakeCheckBackend([]FindResourceRequest{
{Type: ResourceTypeMailingList, Name: "existing@domain2.com"}, {Type: ResourceTypeMailingList, Name: "existing@domain2.com"},
}) })
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment