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 {
func testConfig() *Config {
var c Config
c.ForbiddenUsernames = []string{"root"}
c.AvailableDomains = map[string][]string{
c.Validation.ForbiddenUsernames = []string{"root"}
c.Validation.AvailableDomains = map[string][]string{
ResourceTypeEmail: []string{"example.com"},
ResourceTypeMailingList: []string{"example.com"},
}
......
package accountserver
import (
"errors"
"io/ioutil"
"git.autistici.org/ai3/go-common/clientutil"
......@@ -9,12 +10,7 @@ import (
// Config holds the configuration for the AccountService.
type Config 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"`
Validation ValidationConfig `yaml:",inline"`
Shards struct {
Available map[string][]string `yaml:"available"`
......@@ -34,9 +30,16 @@ type Config struct {
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 {
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)
}
return b
......@@ -58,30 +61,18 @@ func (c *Config) shardBackend() shardBackend {
}
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{
forbiddenUsernames: fu,
forbiddenPasswords: fp,
minPasswordLength: 6,
maxPasswordLength: 128,
webroot: c.WebsiteRootDir,
domains: c.domainBackend(),
shards: c.shardBackend(),
backend: be,
config: &c.Validation,
domains: c.domainBackend(),
shards: c.shardBackend(),
backend: be,
}, nil
}
func (c *Config) templateContext() *templateContext {
return &templateContext{
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
svcConfig.SSO.Domain = testSSODomain
svcConfig.SSO.Service = testSSOService
svcConfig.SSO.AdminGroup = testAdminGroup
svcConfig.ForbiddenUsernames = []string{"forbidden"}
svcConfig.AvailableDomains = map[string][]string{
svcConfig.Validation.ForbiddenUsernames = []string{"forbidden"}
svcConfig.Validation.AvailableDomains = map[string][]string{
as.ResourceTypeEmail: []string{"example.com"},
as.ResourceTypeMailingList: []string{"example.com"},
}
svcConfig.Validation.WebsiteRootDir = "/home/users/investici.org"
shards := []string{"host1", "host2", "host3"}
svcConfig.Shards.Available = map[string][]string{
as.ResourceTypeEmail: shards,
......@@ -139,7 +140,6 @@ func startServiceWithConfig(t testing.TB, svcConfig as.Config) (func(), as.Backe
as.ResourceTypeDatabase: shards,
}
svcConfig.Shards.Allowed = svcConfig.Shards.Available
svcConfig.WebsiteRootDir = "/home/users/investici.org"
service, err := as.NewAccountService(be, &svcConfig)
if err != nil {
......
......@@ -2,6 +2,7 @@ package accountserver
import (
"context"
"fmt"
"log"
"time"
......@@ -103,10 +104,14 @@ func NewAccountService(backend Backend, config *Config) (*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{
authService: newAuthService(config, ssoValidator),
audit: &syslogAuditLogger{},
backend: backend,
authService: newAuthService(config, ssoValidator),
audit: &syslogAuditLogger{},
backend: backend,
enableOpportunisticEncryption: config.EnableOpportunisticEncryption,
}
......
......@@ -27,22 +27,77 @@ type shardBackend interface {
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 various validation functions will need. Most methods on this
// object return functions themselves (ValidatorFunc or variations
// thereof) that can later be called multiple times at will and
// combined with operators like 'allOf'.
type validationContext struct {
forbiddenUsernames stringSet
forbiddenPasswords stringSet
minPasswordLength int
maxPasswordLength int
minUID int
maxUID int
webroot string
domains domainBackend
shards shardBackend
backend Backend
config *ValidationConfig
domains domainBackend
shards shardBackend
backend Backend
}
// A stringSet is just a list of strings with a quick membership test.
......@@ -327,7 +382,12 @@ func (v *validationContext) isAvailableWebsite() ValidatorFunc {
func (v *validationContext) validHostedEmail() ValidatorFunc {
return allOf(
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)),
),
v.isAvailableEmailAddr(),
......@@ -337,7 +397,12 @@ func (v *validationContext) validHostedEmail() ValidatorFunc {
func (v *validationContext) validHostedMailingList() ValidatorFunc {
return allOf(
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)),
),
v.isAvailableEmailAddr(),
......@@ -346,9 +411,9 @@ func (v *validationContext) validHostedMailingList() ValidatorFunc {
func (v *validationContext) validPassword() ValidatorFunc {
return allOf(
minLength(v.minPasswordLength),
maxLength(v.maxPasswordLength),
notInSet(v.forbiddenPasswords),
minLength(v.config.MinPasswordLen),
maxLength(v.config.MaxPasswordLen),
notInSet(v.config.forbiddenPasswords),
)
}
......@@ -473,8 +538,8 @@ func (v *validationContext) validateUID(uid int, user *User) error {
if uid == 0 {
return errors.New("uid is not set")
}
if uid < v.minUID || (v.maxUID > 0 && uid > v.maxUID) {
return errors.New("uid outside of allowed range")
if uid < v.config.MinUID || (v.config.MaxUID > 0 && uid > v.config.MaxUID) {
return fmt.Errorf("uid %d outside of allowed range (%d-%d)", uid, v.config.MinUID, v.config.MaxUID)
}
if user != nil && uid != user.UID {
return errors.New("uid of resource differs from uid of user")
......@@ -489,7 +554,7 @@ func (v *validationContext) validateWebsiteCommon(r *Resource, user *User) error
if r.Website.DocumentRoot == "" {
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")
}
if !hasMatchingDAVAccount(user, r) {
......@@ -577,7 +642,7 @@ func (v *validationContext) validDAVResource() ResourceValidatorFunc {
if r.DAV == nil {
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")
}
......
......@@ -97,10 +97,12 @@ func (f *fakeCheckTX) HasAnyResource(_ context.Context, reqs []FindResourceReque
return false, nil
}
func newTestValidationConfig(entries ...string) *validationContext {
return &validationContext{
forbiddenUsernames: newStringSetFromList(entries),
func newTestValidationContext(forbiddenUsernames ...string) *validationContext {
config := &ValidationConfig{
ForbiddenUsernames: forbiddenUsernames,
}
config.compile()
return &validationContext{config: config}
}
func newFakeDomainBackend(domains ...string) domainBackend {
......@@ -123,7 +125,7 @@ func TestValidator_HostedEmail(t *testing.T) {
{"forbidden@example.com", false},
{"existing@example.com", false},
}
vc := newTestValidationConfig("forbidden")
vc := newTestValidationContext("forbidden")
vc.backend = newFakeCheckBackend([]FindResourceRequest{
{Type: ResourceTypeEmail, Name: "existing@example.com"},
})
......@@ -141,7 +143,7 @@ func TestValidator_HostedMailingList(t *testing.T) {
{"existing@domain1.com", false},
{"existing@domain2.com", false},
}
vc := newTestValidationConfig("forbidden")
vc := newTestValidationContext("forbidden")
vc.backend = newFakeCheckBackend([]FindResourceRequest{
{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