validators.go 34.1 KB
Newer Older
ale's avatar
ale committed
1
2
3
4
5
6
7
package accountserver

import (
	"bufio"
	"context"
	"errors"
	"fmt"
ale's avatar
ale committed
8
	"math/rand"
ale's avatar
ale committed
9
	"os"
ale's avatar
ale committed
10
	"path/filepath"
ale's avatar
ale committed
11
12
	"regexp"
	"strings"
ale's avatar
ale committed
13
	"time"
ale's avatar
ale committed
14
15
16
17
18
19

	"golang.org/x/net/publicsuffix"
)

// A domainBackend manages the list of domains users are allowed to request services on.
type domainBackend interface {
20
	GetAllowedDomains(context.Context, string) []string
21
	IsAllowedDomain(*RequestContext, string, string) bool
ale's avatar
ale committed
22
23
}

ale's avatar
ale committed
24
25
26
27
28
29
30
// A shardBackend can return information about available / allowed service shards.
type shardBackend interface {
	GetAllowedShards(context.Context, string) []string
	GetAvailableShards(context.Context, string) []string
	IsAllowedShard(context.Context, string, string) bool
}

31
32
33
34
35
36
37
// 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"`
38
39
	ForbiddenDomains       []string            `yaml:"forbidden_domains"`
	ForbiddenDomainsFile   string              `yaml:"forbidden_domains_file"`
40
41
42
43
44
45
46
47
48
	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"`

49
	forbiddenUsernames *regexpSet
50
51
	forbiddenPasswords *stringSet
	forbiddenDomains   *regexpSet
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
}

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()

87
	c.forbiddenUsernames, err = newRegexpSetFromFileOrList(c.ForbiddenUsernames, c.ForbiddenUsernamesFile)
88
89
90
91
	if err != nil {
		return
	}
	c.forbiddenPasswords, err = newStringSetFromFileOrList(c.ForbiddenPasswords, c.ForbiddenPasswordsFile)
92
93
94
	if err != nil {
		return
	}
95
	c.forbiddenDomains, err = newRegexpSetFromFileOrList(c.ForbiddenDomains, c.ForbiddenDomainsFile)
96
97
98
	return
}

99
// The validationContext contains all configuration and backends that
ale's avatar
ale committed
100
101
102
103
// 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'.
104
type validationContext struct {
105
106
107
108
	config  *ValidationConfig
	domains domainBackend
	shards  shardBackend
	backend Backend
ale's avatar
ale committed
109
110
111
112
113
114
115
116
}

// A stringSet is just a list of strings with a quick membership test.
type stringSet struct {
	set  map[string]struct{}
	list []string
}

117
func newStringSetFromList(list []string) *stringSet {
ale's avatar
ale committed
118
119
120
121
	set := make(map[string]struct{})
	for _, s := range list {
		set[s] = struct{}{}
	}
122
	return &stringSet{set: set, list: list}
ale's avatar
ale committed
123
124
}

125
func newStringSetFromFileOrList(list []string, path string) (*stringSet, error) {
ale's avatar
ale committed
126
	if path != "" {
127
128
129
130
131
		var err error
		list, err = loadStringsFromFile(path)
		if err != nil {
			return nil, err
		}
ale's avatar
ale committed
132
133
134
135
	}
	return newStringSetFromList(list), nil
}

136
137
138
139
func (s *stringSet) Contains(needle string) bool {
	if s == nil {
		return false
	}
ale's avatar
ale committed
140
141
142
143
	_, ok := s.set[needle]
	return ok
}

144
145
146
147
func (s *stringSet) List() []string {
	if s == nil {
		return nil
	}
ale's avatar
ale committed
148
149
150
	return s.list
}

151
152
153
154
155
156
157
158
// A set of regular expressions.
type regexpSet struct {
	exprs []*regexp.Regexp
}

func newRegexpSetFromList(list []string) (*regexpSet, error) {
	var set regexpSet
	for _, pattern := range list {
159
160
		// Automatically hook the pattern to the beg/end of input.
		rx, err := regexp.Compile(fmt.Sprintf("^%s$", pattern))
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
		if err != nil {
			return nil, err
		}
		set.exprs = append(set.exprs, rx)
	}
	return &set, nil
}

func newRegexpSetFromFileOrList(list []string, path string) (*regexpSet, error) {
	if path != "" {
		var err error
		list, err = loadStringsFromFile(path)
		if err != nil {
			return nil, err
		}
	}
	return newRegexpSetFromList(list)
}

func (s *regexpSet) Contains(needle string) bool {
	for _, rx := range s.exprs {
		if rx.MatchString(needle) {
			return true
		}
	}
	return false
}

ale's avatar
ale committed
189
190
// A domainBackend that works with a static list of type-specific allowed domains.
type staticDomainBackend struct {
191
	sets map[string]*stringSet
ale's avatar
ale committed
192
193
}

194
func (d *staticDomainBackend) GetAllowedDomains(_ context.Context, kind string) []string {
ale's avatar
ale committed
195
196
197
	return d.sets[kind].List()
}

198
func (d *staticDomainBackend) IsAllowedDomain(_ *RequestContext, kind, domain string) bool {
ale's avatar
ale committed
199
200
201
	return d.sets[kind].Contains(domain)
}

ale's avatar
ale committed
202
type staticShardBackend struct {
203
204
	available map[string]*stringSet
	allowed   map[string]*stringSet
ale's avatar
ale committed
205
206
207
208
209
210
211
212
213
214
215
216
217
218
}

func (d *staticShardBackend) GetAllowedShards(_ context.Context, kind string) []string {
	return d.allowed[kind].List()
}

func (d *staticShardBackend) GetAvailableShards(_ context.Context, kind string) []string {
	return d.available[kind].List()
}

func (d *staticShardBackend) IsAllowedShard(_ context.Context, kind, shard string) bool {
	return d.allowed[kind].Contains(shard)
}

219
func loadStringsFromFile(path string) ([]string, error) {
ale's avatar
ale committed
220
	f, err := os.Open(path) // #nosec
ale's avatar
ale committed
221
	if err != nil {
222
		return nil, err
ale's avatar
ale committed
223
	}
ale's avatar
ale committed
224
	defer f.Close() // nolint: errcheck
ale's avatar
ale committed
225
226
227
228
229

	var list []string
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := scanner.Text()
230
		if line == "" || line[0] == '#' {
ale's avatar
ale committed
231
232
233
234
235
			continue
		}
		list = append(list, line)
	}
	if err := scanner.Err(); err != nil {
236
		return nil, err
ale's avatar
ale committed
237
	}
238
	return list, nil
ale's avatar
ale committed
239
240
241
242
}

// ValidatorFunc is the generic interface for unstructured data field
// (string) validators.
243
type ValidatorFunc func(*RequestContext, string) error
ale's avatar
ale committed
244

245
246
247
248
249
type genericSet interface {
	Contains(string) bool
}

func notInSet(set genericSet) ValidatorFunc {
250
	return func(_ *RequestContext, value string) error {
ale's avatar
ale committed
251
		if set.Contains(value) {
252
			return errors.New("invalid value (blacklisted)")
ale's avatar
ale committed
253
254
255
256
257
258
		}
		return nil
	}
}

func minLength(minLen int) ValidatorFunc {
259
	return func(_ *RequestContext, value string) error {
ale's avatar
ale committed
260
261
262
263
264
265
266
267
		if len(value) < minLen {
			return fmt.Errorf("value must be at least %d characters", minLen)
		}
		return nil
	}
}

func maxLength(maxLen int) ValidatorFunc {
268
	return func(_ *RequestContext, value string) error {
ale's avatar
ale committed
269
270
271
272
273
274
275
276
		if len(value) > maxLen {
			return fmt.Errorf("value must be at most %d characters", maxLen)
		}
		return nil
	}
}

func matchRegexp(rx *regexp.Regexp, errmsg string) ValidatorFunc {
277
	return func(_ *RequestContext, value string) error {
ale's avatar
ale committed
278
279
280
281
282
283
284
		if !rx.MatchString(value) {
			return errors.New(errmsg)
		}
		return nil
	}
}

285
// The generic username regexp allows for standard usernames.
ale's avatar
ale committed
286
var usernameRx = regexp.MustCompile(`^([a-z0-9]+[-_.]?)+[a-z0-9]$`)
ale's avatar
ale committed
287
288
289
290
291
292
293
294
295

func matchUsernameRx() ValidatorFunc {
	return matchRegexp(usernameRx, "value must be an alphanumeric sequence, including . and - characters but not starting or ending with those")
}

func matchSitenameRx() ValidatorFunc {
	return matchUsernameRx()
}

296
297
298
299
300
301
302
// The identifier regexp is stricter and forbids [-_.] characters.
var identifierRx = regexp.MustCompile(`^[a-z0-9]+$`)

func matchIdentifierRx() ValidatorFunc {
	return matchRegexp(identifierRx, "value must be alphanumeric")
}

ale's avatar
ale committed
303
func validateUsernameAndDomain(validateUsername, validateDomain ValidatorFunc) ValidatorFunc {
304
	return func(rctx *RequestContext, value string) error {
ale's avatar
ale committed
305
306
307
308
		parts := strings.SplitN(value, "@", 2)
		if len(parts) != 2 {
			return errors.New("malformed email address")
		}
309
		if err := validateUsername(rctx, parts[0]); err != nil {
ale's avatar
ale committed
310
311
			return err
		}
312
		return validateDomain(rctx, parts[1])
ale's avatar
ale committed
313
314
315
316
317
318
319
320
321
322
323
324
	}
}

// This is not a prescriptive domain name validation regex, it's just used to
// give useful feedback to the user if the domain name looks visibly wrong.
var domainRx = regexp.MustCompile(`^(?:[a-zA-Z0-9_\-]{1,63}\.)+(?:[a-zA-Z]{2,})$`)

func isRegistered(domain string) bool {
	// TODO: ha ha, there isn't really a good way to check.
	return true
}

325
func validDomainName(_ *RequestContext, value string) error {
ale's avatar
ale committed
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
	if !domainRx.MatchString(value) {
		return errors.New("invalid domain name")
	}

	// Extract the top-level domain and check that it is registered
	// (which works also if we're hosting the top-level domain
	// ourselves already).
	domain, err := publicsuffix.EffectiveTLDPlusOne(value)
	if err != nil {
		return errors.New("invalid TLD")
	}
	if !isRegistered(domain) {
		return fmt.Errorf("the domain %s does not seem to be registered", domain)
	}
	return nil
}

// Split a (correctly formed) email address into username/domain.
func splitEmailAddr(addr string) (string, string) {
	parts := strings.SplitN(addr, "@", 2)
	return parts[0], parts[1]
}

// Returns all the possible resources in the email and mailing list
// namespaces that might conflict with the given email address.
ale's avatar
ale committed
351
func relatedEmails(ctx context.Context, be domainBackend, addr string) []FindResourceRequest {
352
	// Check for the literal addr in the unified mail+list namespace.
ale's avatar
ale committed
353
354
	rel := []FindResourceRequest{
		{Type: ResourceTypeEmail, Name: addr},
355
356
		{Type: ResourceTypeMailingList, Name: addr},
		{Type: ResourceTypeNewsletter, Name: addr},
ale's avatar
ale committed
357
	}
358

359
360
361
362
	// Mailing lists and newsletters must have unique names
	// regardless of the domain, so we add potential conflicts for
	// mailing lists with the same name over all list-enabled
	// domains.
363
	user, _ := splitEmailAddr(addr)
364
	for _, d := range be.GetAllowedDomains(ctx, ResourceTypeMailingList) {
ale's avatar
ale committed
365
366
367
		rel = append(rel, FindResourceRequest{
			Type: ResourceTypeMailingList,
			Name: fmt.Sprintf("%s@%s", user, d),
368
369
370
371
372
373
		})
	}
	for _, d := range be.GetAllowedDomains(ctx, ResourceTypeNewsletter) {
		rel = append(rel, FindResourceRequest{
			Type: ResourceTypeNewsletter,
			Name: fmt.Sprintf("%s@%s", user, d),
ale's avatar
ale committed
374
		})
ale's avatar
ale committed
375
	}
ale's avatar
ale committed
376
	return rel
ale's avatar
ale committed
377
378
}

379
380
381
382
383
384
385
386
func relatedWebsites(ctx context.Context, be domainBackend, value string) []FindResourceRequest {
	// Ignore the parent domain (websites share a global namespace).
	return []FindResourceRequest{
		{
			Type: ResourceTypeWebsite,
			Name: value,
		},
	}
ale's avatar
ale committed
387
388
}

389
390
391
func relatedDomains(ctx context.Context, be domainBackend, value string) []FindResourceRequest {
	return []FindResourceRequest{
		{
392
393
			Type: ResourceTypeDomain,
			Name: value,
394
		},
ale's avatar
ale committed
395
396
397
	}
}

398
func (v *validationContext) isAllowedDomain(rtype string) ValidatorFunc {
399
400
	return func(rctx *RequestContext, value string) error {
		if !v.domains.IsAllowedDomain(rctx, rtype, value) {
ale's avatar
ale committed
401
402
403
404
405
406
			return errors.New("unavailable domain")
		}
		return nil
	}
}

407
408
409
410
411
412
413
414
415
416
417
418
419
420
func (v *validationContext) isAllowedEmailDomain() ValidatorFunc {
	return func(rctx *RequestContext, value string) error {
		// Static lookup first.
		if v.domains.IsAllowedDomain(rctx, ResourceTypeEmail, value) {
			return nil
		}

		// Dynamic lookup for hosted domains in the database
		// that have AcceptMail=true.
		if tx, err := v.backend.NewTransaction(); err == nil {

			// Ignore the error in FindResource() since we're going to fail close anyway.
			// nolint: errcheck
			rsrc, _ := tx.FindResource(rctx.Context, FindResourceRequest{Type: ResourceTypeDomain, Name: value})
ale's avatar
ale committed
421
			if rsrc != nil && rsrc.Status == ResourceStatusActive && rsrc.Website.AcceptMail {
422
423
424
425
426
427
428
429
				return nil
			}
		}

		return errors.New("unavailable domain")
	}
}

430
func (v *validationContext) isAvailableEmailAddr() ValidatorFunc {
431
432
	return func(rctx *RequestContext, value string) error {
		rel := relatedEmails(rctx.Context, v.domains, value)
433
434
435
436
437
438
439

		// Run the presence check in a new transaction. Unavailability
		// of the server results in a validation error (fail close).
		tx, err := v.backend.NewTransaction()
		if err != nil {
			return err
		}
ale's avatar
ale committed
440
		// Errors will cause to consider the address unavailable.
441
		if ok, _ := tx.HasAnyResource(rctx.Context, rel); ok { // nolint
442
			return errors.New("address unavailable")
ale's avatar
ale committed
443
444
445
446
447
		}
		return nil
	}
}

448
func (v *validationContext) isAvailableDomain() ValidatorFunc {
449
450
	return func(rctx *RequestContext, value string) error {
		rel := relatedDomains(rctx.Context, v.domains, value)
ale's avatar
ale committed
451
452
453

		// Run the presence check in a new transaction. Unavailability
		// of the server results in a validation error (fail close).
454
		tx, err := v.backend.NewTransaction()
ale's avatar
ale committed
455
456
457
		if err != nil {
			return err
		}
ale's avatar
ale committed
458
		// Errors will cause to consider the resource unavailable.
459
		if ok, _ := tx.HasAnyResource(rctx.Context, rel); ok { // nolint
ale's avatar
ale committed
460
461
462
463
464
465
			return errors.New("address unavailable")
		}
		return nil
	}
}

466
func (v *validationContext) isAvailableWebsite() ValidatorFunc {
467
468
	return func(rctx *RequestContext, value string) error {
		rel := relatedWebsites(rctx.Context, v.domains, value)
ale's avatar
ale committed
469
470
471

		// Run the presence check in a new transaction. Unavailability
		// of the server results in a validation error (fail close).
472
		tx, err := v.backend.NewTransaction()
ale's avatar
ale committed
473
474
475
		if err != nil {
			return err
		}
ale's avatar
ale committed
476
		// Errors will cause to consider the resource unavailable.
477
		if ok, _ := tx.HasAnyResource(rctx.Context, rel); ok { // nolint
ale's avatar
ale committed
478
479
480
481
482
483
			return errors.New("address unavailable")
		}
		return nil
	}
}

484
func (v *validationContext) isAvailableDAV() ValidatorFunc {
485
	return func(rctx *RequestContext, value string) error {
486
487
488
489
490
491
492
493
494
495
496
497
498
499
		rel := []FindResourceRequest{
			{
				Type: ResourceTypeDAV,
				Name: value,
			},
		}

		// Run the presence check in a new transaction. Unavailability
		// of the server results in a validation error (fail close).
		tx, err := v.backend.NewTransaction()
		if err != nil {
			return err
		}
		// Errors will cause to consider the resource unavailable.
500
		if ok, _ := tx.HasAnyResource(rctx.Context, rel); ok { // nolint
501
502
503
504
505
506
507
			return errors.New("name unavailable")
		}
		return nil
	}
}

func (v *validationContext) isAvailableDatabase() ValidatorFunc {
508
	return func(rctx *RequestContext, value string) error {
509
510
511
512
513
514
515
516
517
518
519
520
521
522
		rel := []FindResourceRequest{
			{
				Type: ResourceTypeDatabase,
				Name: value,
			},
		}

		// Run the presence check in a new transaction. Unavailability
		// of the server results in a validation error (fail close).
		tx, err := v.backend.NewTransaction()
		if err != nil {
			return err
		}
		// Errors will cause to consider the resource unavailable.
523
		if ok, _ := tx.HasAnyResource(rctx.Context, rel); ok { // nolint
524
525
526
527
528
529
			return errors.New("name unavailable")
		}
		return nil
	}
}

530
func (v *validationContext) validHostedEmail() ValidatorFunc {
ale's avatar
ale committed
531
532
533
534
535
536
	return validateUsernameAndDomain(
		allOf(
			matchUsernameRx(),
			minLength(v.config.MinUsernameLen),
			maxLength(v.config.MaxUsernameLen),
			notInSet(v.config.forbiddenUsernames),
ale's avatar
ale committed
537
		),
538
		allOf(v.isAllowedEmailDomain()),
ale's avatar
ale committed
539
540
541
	)
}

542
543
544
545
546
547
548
func (v *validationContext) validHostedNewEmail() ValidatorFunc {
	return allOf(
		v.validHostedEmail(),
		v.isAvailableEmailAddr(),
	)
}

549
func (v *validationContext) validHostedMailingList() ValidatorFunc {
ale's avatar
ale committed
550
551
552
553
554
555
	return validateUsernameAndDomain(
		allOf(
			matchUsernameRx(),
			minLength(v.config.MinUsernameLen),
			maxLength(v.config.MaxUsernameLen),
			notInSet(v.config.forbiddenUsernames),
ale's avatar
ale committed
556
		),
ale's avatar
ale committed
557
		allOf(v.isAllowedDomain(ResourceTypeMailingList)),
ale's avatar
ale committed
558
559
560
	)
}

561
func (v *validationContext) validPassword() ValidatorFunc {
ale's avatar
ale committed
562
	return allOf(
563
564
565
		minLength(v.config.MinPasswordLen),
		maxLength(v.config.MaxPasswordLen),
		notInSet(v.config.forbiddenPasswords),
566
567
568
	)
}

ale's avatar
ale committed
569
func allOf(funcs ...ValidatorFunc) ValidatorFunc {
570
	return func(rctx *RequestContext, value string) error {
ale's avatar
ale committed
571
		for _, f := range funcs {
572
			if err := f(rctx, value); err != nil {
ale's avatar
ale committed
573
574
575
576
577
578
				return err
			}
		}
		return nil
	}
}
ale's avatar
ale committed
579
580
581

// ResourceValidatorFunc is a composite type validator that checks
// various fields in a Resource, depending on its type.
582
type ResourceValidatorFunc func(*RequestContext, *Resource, *User, bool) error
ale's avatar
ale committed
583

584
func (v *validationContext) validateResource(_ *RequestContext, r *Resource, user *User) error {
ale's avatar
ale committed
585
586
	// Validate the status enum.
	switch r.Status {
ale's avatar
ale committed
587
	case ResourceStatusActive, ResourceStatusInactive, ResourceStatusReadonly, ResourceStatusArchived:
ale's avatar
ale committed
588
	default:
ale's avatar
ale committed
589
		return newValidationError(nil, "status", "unknown resource status")
ale's avatar
ale committed
590
591
592
593
594
595
	}

	// If the resource has a ParentID, it must reference another
	// resource owned by the user.
	if !r.ParentID.Empty() {
		if user == nil {
ale's avatar
ale committed
596
			return newValidationError(nil, "parent_id", "resource can't have parent without user context")
ale's avatar
ale committed
597
598
		}
		if p := user.GetResourceByID(r.ParentID); p == nil {
ale's avatar
ale committed
599
			return newValidationError(nil, "parent_id", "parent references unknown resource")
ale's avatar
ale committed
600
601
602
603
604
605
		}
	}

	return nil
}

606
607
func (v *validationContext) validateShardedResource(rctx *RequestContext, r *Resource, user *User) error {
	if err := v.validateResource(rctx, r, user); err != nil {
ale's avatar
ale committed
608
609
		return err
	}
610
	if r.Shard == "" {
ale's avatar
ale committed
611
		return newValidationError(nil, "shard", "empty shard")
612
	}
613
	if !v.shards.IsAllowedShard(rctx.Context, r.Type, r.Shard) {
ale's avatar
ale committed
614
		return newValidationError(nil, "shard", fmt.Sprintf(
ale's avatar
ale committed
615
616
			"invalid shard %s for resource type %s (allowed: %v)",
			r.Shard,
ale's avatar
ale committed
617
			r.Type,
618
			v.shards.GetAllowedShards(rctx.Context, r.Type),
ale's avatar
ale committed
619
		))
ale's avatar
ale committed
620
621
	}
	if r.OriginalShard == "" {
ale's avatar
ale committed
622
		return newValidationError(nil, "original_shard", "empty original_shard")
ale's avatar
ale committed
623
624
625
	}
	return nil
}
ale's avatar
ale committed
626

627
628
func (v *validationContext) validEmailResource() ResourceValidatorFunc {
	emailValidator := v.validHostedEmail()
629
	newEmailValidator := v.validHostedNewEmail()
ale's avatar
ale committed
630

631
632
	return func(rctx *RequestContext, r *Resource, user *User, isNew bool) error {
		if err := v.validateShardedResource(rctx, r, user); err != nil {
ale's avatar
ale committed
633
634
635
636
637
			return err
		}

		// Email resources aren't nested.
		if !r.ParentID.Empty() {
ale's avatar
ale committed
638
			return newValidationError(nil, "parent_id", "resource should not have parent")
ale's avatar
ale committed
639
640
641
		}

		if r.Email == nil {
ale's avatar
ale committed
642
			return newValidationError(nil, "email", "resource has no email metadata")
ale's avatar
ale committed
643
		}
ale's avatar
ale committed
644
645
646

		var err error
		if isNew {
647
			err = newEmailValidator(rctx, r.Name)
ale's avatar
ale committed
648
		} else {
649
			err = emailValidator(rctx, r.Name)
ale's avatar
ale committed
650
651
		}
		if err != nil {
ale's avatar
ale committed
652
			return newValidationError(nil, "name", err.Error())
ale's avatar
ale committed
653
		}
ale's avatar
ale committed
654

ale's avatar
ale committed
655
		if r.Email.Maildir == "" {
ale's avatar
ale committed
656
			return newValidationError(nil, "email.maildir", "empty maildir")
ale's avatar
ale committed
657
658
		}
		return nil
ale's avatar
ale committed
659
660
661
	}
}

662
663
func (v *validationContext) validListResource() ResourceValidatorFunc {
	listValidator := v.validHostedMailingList()
ale's avatar
ale committed
664
	newListValidator := allOf(listValidator, v.isAvailableEmailAddr())
ale's avatar
ale committed
665

666
667
	return func(rctx *RequestContext, r *Resource, user *User, isNew bool) error {
		if err := v.validateShardedResource(rctx, r, user); err != nil {
ale's avatar
ale committed
668
669
670
			return err
		}
		if r.List == nil {
ale's avatar
ale committed
671
			return newValidationError(nil, "list", "resource has no list metadata")
ale's avatar
ale committed
672
		}
ale's avatar
ale committed
673
674
675

		var err error
		if isNew {
676
			err = newListValidator(rctx, r.Name)
ale's avatar
ale committed
677
		} else {
678
			err = listValidator(rctx, r.Name)
ale's avatar
ale committed
679
680
		}
		if err != nil {
ale's avatar
ale committed
681
			return newValidationError(nil, "name", err.Error())
ale's avatar
ale committed
682
		}
ale's avatar
ale committed
683

ale's avatar
ale committed
684
		if len(r.List.Admins) < 1 {
ale's avatar
ale committed
685
			return newValidationError(nil, "list.admins", "can't create a list without admins")
ale's avatar
ale committed
686
687
688
689
690
		}
		return nil
	}
}

691
692
func (v *validationContext) validNewsletterResource() ResourceValidatorFunc {
	listValidator := v.validHostedMailingList()
ale's avatar
ale committed
693
	newListValidator := allOf(listValidator, v.isAvailableEmailAddr())
694

695
696
	return func(rctx *RequestContext, r *Resource, user *User, isNew bool) error {
		if err := v.validateShardedResource(rctx, r, user); err != nil {
697
698
699
			return err
		}
		if r.Newsletter == nil {
ale's avatar
ale committed
700
			return newValidationError(nil, "newsletter", "resource has no newsletter metadata")
701
		}
ale's avatar
ale committed
702
703
704

		var err error
		if isNew {
705
			err = newListValidator(rctx, r.Name)
ale's avatar
ale committed
706
		} else {
707
			err = listValidator(rctx, r.Name)
ale's avatar
ale committed
708
709
		}
		if err != nil {
ale's avatar
ale committed
710
			return newValidationError(nil, "name", err.Error())
711
		}
ale's avatar
ale committed
712

713
		if len(r.Newsletter.Admins) < 1 {
ale's avatar
ale committed
714
			return newValidationError(nil, "newsletter.admins", "can't create a newsletter without admins")
715
716
717
718
719
		}
		return nil
	}
}

ale's avatar
ale committed
720
func findMatchingDAVAccount(user *User, r *Resource) *Resource {
ale's avatar
ale committed
721
	for _, dav := range user.GetResourcesByType(ResourceTypeDAV) {
ale's avatar
ale committed
722
		if isSubdir(dav.DAV.Homedir, r.Website.DocumentRoot) {
723
			return dav
ale's avatar
ale committed
724
725
		}
	}
ale's avatar
ale committed
726
727
728
729
730
	return nil
}

func hasMatchingDAVAccount(user *User, r *Resource) bool {
	return findMatchingDAVAccount(user, r) != nil
ale's avatar
ale committed
731
732
}

ale's avatar
ale committed
733
734
735
736
func (v *validationContext) validateUID(uid int, user *User) error {
	if uid == 0 {
		return errors.New("uid is not set")
	}
737
738
	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)
ale's avatar
ale committed
739
740
741
742
743
744
745
746
747
748
749
750
	}
	if user != nil && uid != user.UID {
		return errors.New("uid of resource differs from uid of user")
	}
	return nil
}

func (v *validationContext) validateWebsiteCommon(r *Resource, user *User) error {
	// Document root checks: must not be empty, and the
	// user must have a DAV account with a home directory
	// that is a parent of this document root.
	if r.Website.DocumentRoot == "" {
ale's avatar
ale committed
751
		return newValidationError(nil, "document_root", "empty document_root")
ale's avatar
ale committed
752
	}
753
	if !isSubdir(v.config.WebsiteRootDir, r.Website.DocumentRoot) {
ale's avatar
ale committed
754
		return newValidationError(nil, "document_root", "document root outside of web root")
ale's avatar
ale committed
755
756
	}
	if !hasMatchingDAVAccount(user, r) {
ale's avatar
ale committed
757
		return newValidationError(nil, "website", "website has no matching DAV account")
ale's avatar
ale committed
758
759
760
	}

	// UID checks.
ale's avatar
ale committed
761
762
763
764
	if err := v.validateUID(r.Website.UID, user); err != nil {
		return newValidationError(nil, "website.uid", err.Error())
	}
	return nil
ale's avatar
ale committed
765
766
}

767
768
769
770
func (v *validationContext) validDomainResource() ResourceValidatorFunc {
	domainValidator := allOf(
		minLength(6),
		validDomainName,
771
		notInSet(v.config.forbiddenDomains),
ale's avatar
ale committed
772
773
774
	)
	newDomainValidator := allOf(
		domainValidator,
775
776
		v.isAvailableDomain(),
	)
ale's avatar
ale committed
777

778
779
	return func(rctx *RequestContext, r *Resource, user *User, isNew bool) error {
		if err := v.validateShardedResource(rctx, r, user); err != nil {
ale's avatar
ale committed
780
781
782
783
784
785
786
787
788
789
790
			return err
		}

		// Web resources aren't nested.
		if !r.ParentID.Empty() {
			return errors.New("resource should not have parent")
		}

		if r.Website == nil {
			return errors.New("resource has no website metadata")
		}
ale's avatar
ale committed
791
792
793

		var err error
		if isNew {
794
			err = newDomainValidator(rctx, r.Name)
ale's avatar
ale committed
795
		} else {
796
			err = domainValidator(rctx, r.Name)
ale's avatar
ale committed
797
798
		}
		if err != nil {
ale's avatar
ale committed
799
			return newValidationError(nil, "name", err.Error())
ale's avatar
ale committed
800
		}
ale's avatar
ale committed
801

ale's avatar
ale committed
802
		if r.Website.ParentDomain != "" {
ale's avatar
ale committed
803
			return newValidationError(nil, "parent_domain", "non-empty parent_domain on domain resource")
ale's avatar
ale committed
804
805
		}

ale's avatar
ale committed
806
		return v.validateWebsiteCommon(r, user)
ale's avatar
ale committed
807
808
809
	}
}

810
811
812
813
func (v *validationContext) validWebsiteResource() ResourceValidatorFunc {
	nameValidator := allOf(
		minLength(6),
		matchSitenameRx(),
ale's avatar
ale committed
814
815
816
	)
	newNameValidator := allOf(
		nameValidator,
817
818
819
820
		v.isAvailableWebsite(),
	)
	parentValidator := v.isAllowedDomain(ResourceTypeWebsite)

821
822
	return func(rctx *RequestContext, r *Resource, user *User, isNew bool) error {
		if err := v.validateShardedResource(rctx, r, user); err != nil {
ale's avatar
ale committed
823
824
825
826
827
828
829
830
831
832
833
			return err
		}

		// Web resources aren't nested.
		if !r.ParentID.Empty() {
			return errors.New("resource should not have parent")
		}

		if r.Website == nil {
			return errors.New("resource has no website metadata")
		}
ale's avatar
ale committed
834
835
836

		var err error
		if isNew {
837
			err = newNameValidator(rctx, r.Name)
ale's avatar
ale committed
838
		} else {
839
			err = nameValidator(rctx, r.Name)
ale's avatar
ale committed
840
841
		}
		if err != nil {
ale's avatar
ale committed
842
			return newValidationError(nil, "name", err.Error())
843
		}
844
		if err := parentValidator(rctx, r.Website.ParentDomain); err != nil {
ale's avatar
ale committed
845
846
847
			return err
		}

ale's avatar
ale committed
848
		return v.validateWebsiteCommon(r, user)
ale's avatar
ale committed
849
850
851
852
	}
}

func (v *validationContext) validDAVResource() ResourceValidatorFunc {
ale's avatar
ale committed
853
	davValidator := allOf(
854
855
		minLength(4),
		matchIdentifierRx(),
ale's avatar
ale committed
856
857
858
	)
	newDAVValidator := allOf(
		davValidator,
859
860
		v.isAvailableDAV(),
	)
861
862
	return func(rctx *RequestContext, r *Resource, user *User, isNew bool) error {
		if err := v.validateShardedResource(rctx, r, user); err != nil {
ale's avatar
ale committed
863
864
865
866
867
868
869
870
			return err
		}

		// DAV resources aren't nested.
		if !r.ParentID.Empty() {
			return errors.New("resource should not have parent")
		}

ale's avatar
ale committed
871
872
		var err error
		if isNew {
873
			err = newDAVValidator(rctx, r.Name)
ale's avatar
ale committed
874
		} else {
875
			err = davValidator(rctx, r.Name)
ale's avatar
ale committed
876
877
		}
		if err != nil {
ale's avatar
ale committed
878
			return newValidationError(nil, "name", err.Error())
879
		}
ale's avatar
ale committed
880

ale's avatar
ale committed
881
882
883
		if r.DAV == nil {
			return errors.New("resource has no dav metadata")
		}
884
		if !isSubdir(v.config.WebsiteRootDir, r.DAV.Homedir) {
ale's avatar
ale committed
885
			return newValidationError(nil, "homedir", "homedir outside of web root")
ale's avatar
ale committed
886
		}
ale's avatar
ale committed
887

ale's avatar
ale committed
888
889
890
891
		if err := v.validateUID(r.DAV.UID, user); err != nil {
			return newValidationError(nil, "dav.uid", err.Error())
		}
		return nil
ale's avatar
ale committed
892
893
894
895
	}
}

func (v *validationContext) validDatabaseResource() ResourceValidatorFunc {
ale's avatar
ale committed
896
	dbValidator := allOf(
897
898
		minLength(4),
		matchIdentifierRx(),
ale's avatar
ale committed
899
900
901
	)
	newDBValidator := allOf(
		dbValidator,
902
903
		v.isAvailableDatabase(),
	)
904
905
	return func(rctx *RequestContext, r *Resource, user *User, isNew bool) error {
		if err := v.validateShardedResource(rctx, r, user); err != nil {
ale's avatar
ale committed
906
907
908
909
910
911
912
			return err
		}

		// Database resources must be nested below a website.
		if r.ParentID.Empty() {
			return errors.New("database resources should be nested")
		}
ale's avatar
ale committed
913
914
915
916
917
918
919

		// TODO: move to global validation
		// switch r.ParentID.Type() {
		// case ResourceTypeWebsite, ResourceTypeDomain:
		// default:
		// 	return errors.New("database parent is not a website resource")
		// }
ale's avatar
ale committed
920

ale's avatar
ale committed
921
922
		var err error
		if isNew {
923
			err = newDBValidator(rctx, r.Name)
ale's avatar
ale committed
924
		} else {
925
			err = dbValidator(rctx, r.Name)
ale's avatar
ale committed
926
927
		}
		if err != nil {
ale's avatar
ale committed
928
			return newValidationError(nil, "name", err.Error())
929
		}
ale's avatar
ale committed
930

ale's avatar
ale committed
931
932
933
934
		if r.Database == nil {
			return errors.New("resource has no database metadata")
		}
		return nil
935
936
937
938
	}
}

// Validator for arbitrary resource types.
ale's avatar
ale committed
939
type resourceValidator struct {
940
	rvs map[string]ResourceValidatorFunc
ale's avatar
ale committed
941
942
}

943
func newResourceValidator(v *validationContext) *resourceValidator {
ale's avatar
ale committed
944
	return &resourceValidator{
945
946
947
		rvs: map[string]ResourceValidatorFunc{
			ResourceTypeEmail:       v.validEmailResource(),
			ResourceTypeMailingList: v.validListResource(),
948
			ResourceTypeNewsletter:  v.validNewsletterResource(),
949
950
			ResourceTypeDomain:      v.validDomainResource(),
			ResourceTypeWebsite:     v.validWebsiteResource(),
ale's avatar
ale committed
951
952
			ResourceTypeDAV:         v.validDAVResource(),
			ResourceTypeDatabase:    v.validDatabaseResource(),
ale's avatar
ale committed
953
954
955
956
		},
	}
}

957
func (v *resourceValidator) validateResource(rctx *RequestContext, r *Resource, user *User, isNew bool) error {
ale's avatar
ale committed
958
959
	// Obvious basic sanity checks on the Resource parameters.
	if r.Name == "" {
ale's avatar
ale committed
960
		return newValidationError(nil, "name", "resource name unset")
ale's avatar
ale committed
961
962
	}
	if r.Type == "" {
ale's avatar
ale committed
963
		return newValidationError(nil, "type", "resource type unset")
ale's avatar
ale committed
964
965
966
967
	}

	rv, ok := v.rvs[r.Type]
	if !ok {
ale's avatar
ale committed
968
		return newValidationError(nil, "type", fmt.Sprintf("unknown resource type '%s'", r.Type))
ale's avatar
ale committed
969
970
	}

971
	return rv(rctx, r, user, isNew)
972
973
974
975
976
}

// Common validators for specific field types.
type fieldValidators struct {
	password ValidatorFunc
977
	newEmail ValidatorFunc
978
979
980
981
982
}

func newFieldValidators(v *validationContext) *fieldValidators {
	return &fieldValidators{
		password: v.validPassword(),
983
		newEmail: v.validHostedNewEmail(),
984
	}
ale's avatar
ale committed
985
}
ale's avatar
ale committed
986

ale's avatar
ale committed
987
// UserValidatorFunc is a compound validator for User objects.
988
type UserValidatorFunc func(*RequestContext, *User, bool) error
ale's avatar
ale committed
989

990
991
992
993
994
995
996
997
998
999
1000
// Verify that user-level invariants are respected. This check can be applied
// to new or existing objects.
//
// nolint: gocyclo
func checkUserInvariants(user *User) error {
	// Count email resources. An account must have exactly 1 email
	// resource.
	var email *Resource
	var emailCount int
	for _, rsrc := range user.Resources {
		if rsrc.Type == ResourceTypeEmail {
For faster browsing, not all history is shown. View entire blame