resources.go 15 KB
Newer Older
ale's avatar
ale committed
1
2
3
4
5
package backend

import (
	"errors"
	"fmt"
ale's avatar
ale committed
6
	"strconv"
ale's avatar
ale committed
7
8
9
10
	"strings"

	"gopkg.in/ldap.v2"

11
	as "git.autistici.org/ai3/accountserver"
ale's avatar
ale committed
12
13
14
15
16
)

// Generic resource handler interface. One for each resource type,
// mapping to exactly one LDAP object type.
type resourceHandler interface {
17
18
19
	GetDN(as.ResourceID) (string, error)
	ToLDAP(*as.Resource) []ldap.PartialAttribute
	FromLDAP(*ldap.Entry) (*as.Resource, error)
20
	SearchQuery() *queryTemplate
ale's avatar
ale committed
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
}

// Registry for demultiplexing resource handling. Has a similar
// interface to a resourceHandler, with a few exceptions.
type resourceRegistry struct {
	handlers map[string]resourceHandler
}

func newResourceRegistry() *resourceRegistry {
	return &resourceRegistry{
		handlers: make(map[string]resourceHandler),
	}
}

func (reg *resourceRegistry) register(rtype string, h resourceHandler) {
	if reg.handlers == nil {
		reg.handlers = make(map[string]resourceHandler)
	}
	reg.handlers[rtype] = h
}

func (reg *resourceRegistry) dispatch(rsrcType string, f func(resourceHandler) error) error {
	h, ok := reg.handlers[rsrcType]
	if !ok {
		return errors.New("unknown resource type")
	}
	return f(h)
}

50
func (reg *resourceRegistry) GetDN(id as.ResourceID) (s string, err error) {
ale's avatar
ale committed
51
52
53
54
55
56
57
	err = reg.dispatch(id.Type(), func(h resourceHandler) (herr error) {
		s, herr = h.GetDN(id)
		return
	})
	return
}

58
func (reg *resourceRegistry) ToLDAP(rsrc *as.Resource) (attrs []ldap.PartialAttribute) {
ale's avatar
ale committed
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
	if err := reg.dispatch(rsrc.ID.Type(), func(h resourceHandler) error {
		attrs = h.ToLDAP(rsrc)
		return nil
	}); err != nil {
		return nil
	}

	attrs = append(attrs, []ldap.PartialAttribute{
		{Type: "status", Vals: s2l(rsrc.Status)},
		{Type: "host", Vals: s2l(rsrc.Shard)},
		{Type: "originalHost", Vals: s2l(rsrc.OriginalShard)},
	}...)
	return
}

74
func setCommonResourceAttrs(entry *ldap.Entry, rsrc *as.Resource) {
ale's avatar
ale committed
75
76
77
78
79
	rsrc.Status = entry.GetAttributeValue("status")
	rsrc.Shard = entry.GetAttributeValue("host")
	rsrc.OriginalShard = entry.GetAttributeValue("originalHost")
}

80
func (reg *resourceRegistry) FromLDAP(entry *ldap.Entry) (rsrc *as.Resource, err error) {
ale's avatar
ale committed
81
82
83
84
85
86
87
88
89
90
91
92
	// Since we don't know what resource type to expect, we try
	// all known handlers until one returns a valid Resource.
	for _, h := range reg.handlers {
		rsrc, err = h.FromLDAP(entry)
		if err == nil {
			setCommonResourceAttrs(entry, rsrc)
			return
		}
	}
	return nil, errors.New("unknown resource")
}

93
func (reg *resourceRegistry) FromLDAPWithType(rsrcType string, entry *ldap.Entry) (rsrc *as.Resource, err error) {
ale's avatar
ale committed
94
95
96
97
98
99
100
101
102
103
104
	err = reg.dispatch(rsrcType, func(h resourceHandler) (rerr error) {
		rsrc, rerr = h.FromLDAP(entry)
		if rerr != nil {
			return
		}
		setCommonResourceAttrs(entry, rsrc)
		return
	})
	return
}

105
func (reg *resourceRegistry) SearchQuery(rsrcType string) (q *queryTemplate, err error) {
ale's avatar
ale committed
106
	err = reg.dispatch(rsrcType, func(h resourceHandler) error {
107
		q = h.SearchQuery()
ale's avatar
ale committed
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
		return nil
	})
	return
}

// Find the parent RDN, which is expected to have the specified
// attribute, and return its value.
func getParentRDN(dn, parentAttr string) (string, error) {
	parsed, err := ldap.ParseDN(dn)
	if err != nil {
		return "", err
	}
	if len(parsed.RDNs) < 2 {
		return "", errors.New("not enough DN components to find parent")
	}
	if parsed.RDNs[1].Attributes[0].Type != parentAttr {
		return "", errors.New("parent RDN has unexpected type")
	}
	return parsed.RDNs[1].Attributes[0].Value, nil
}

// Email resource.
type emailResourceHandler struct {
	baseDN string
}

134
func (h *emailResourceHandler) GetDN(id as.ResourceID) (string, error) {
ale's avatar
ale committed
135
136
137
138
139
140
141
142
143
144
145
146
	if id.User() == "" {
		return "", errors.New("unqualified resource id")
	}
	dn := replaceVars("mail=${resource},uid=${user},ou=People", map[string]string{
		"user":     id.User(),
		"resource": id.Name(),
	})
	return joinDN(dn, h.baseDN), nil
}

var errWrongObjectClass = errors.New("objectClass does not match")

147
func (h *emailResourceHandler) FromLDAP(entry *ldap.Entry) (*as.Resource, error) {
ale's avatar
ale committed
148
149
150
151
152
153
154
155
156
157
	if !isObjectClass(entry, "virtualMailUser") {
		return nil, errWrongObjectClass
	}

	email := entry.GetAttributeValue("mail")
	username, err := getParentRDN(entry.DN, "uid")
	if err != nil {
		return nil, err
	}

158
159
160
	return &as.Resource{
		ID: as.NewResourceID(
			as.ResourceTypeEmail,
ale's avatar
ale committed
161
162
163
			username,
			email,
		),
164
		Type: as.ResourceTypeEmail,
ale's avatar
ale committed
165
		Name: email,
166
		Email: &as.Email{
ale's avatar
ale committed
167
			Aliases: entry.GetAttributeValues("mailAlternateAddress"),
ale's avatar
ale committed
168
169
170
171
172
			Maildir: entry.GetAttributeValue("mailMessageStore"),
		},
	}, nil
}

173
func (h *emailResourceHandler) ToLDAP(rsrc *as.Resource) []ldap.PartialAttribute {
ale's avatar
ale committed
174
175
176
	return []ldap.PartialAttribute{
		{Type: "objectClass", Vals: []string{"top", "virtualMailUser"}},
		{Type: "mail", Vals: s2l(rsrc.ID.Name())},
ale's avatar
ale committed
177
		{Type: "mailAlternateAddress", Vals: rsrc.Email.Aliases},
ale's avatar
ale committed
178
179
180
181
		{Type: "mailMessageStore", Vals: s2l(rsrc.Email.Maildir)},
	}
}

182
183
func (h *emailResourceHandler) SearchQuery() *queryTemplate {
	return &queryTemplate{
ale's avatar
ale committed
184
185
		Base:   joinDN("ou=People", h.baseDN),
		Filter: "(&(objectClass=virtualMailUser)(mail=${resource}))",
186
187
		Scope:  ldap.ScopeWholeSubtree,
	}
ale's avatar
ale committed
188
189
190
191
192
193
194
}

// Mailing list resource.
type mailingListResourceHandler struct {
	baseDN string
}

195
func (h *mailingListResourceHandler) GetDN(id as.ResourceID) (string, error) {
ale's avatar
ale committed
196
197
198
199
200
201
	dn := replaceVars("listName=${resource},ou=Lists", map[string]string{
		"resource": id.Name(),
	})
	return joinDN(dn, h.baseDN), nil
}

202
func (h *mailingListResourceHandler) FromLDAP(entry *ldap.Entry) (*as.Resource, error) {
ale's avatar
ale committed
203
204
205
206
207
	if !isObjectClass(entry, "mailingList") {
		return nil, errWrongObjectClass
	}

	listName := entry.GetAttributeValue("listName")
208
209
210
	return &as.Resource{
		ID:   as.NewResourceID(as.ResourceTypeMailingList, listName),
		Type: as.ResourceTypeMailingList,
ale's avatar
ale committed
211
		Name: listName,
212
		List: &as.MailingList{
ale's avatar
ale committed
213
214
215
216
217
218
			Public: s2b(entry.GetAttributeValue("public")),
			Admins: entry.GetAttributeValues("listOwner"),
		},
	}, nil
}

219
func (h *mailingListResourceHandler) ToLDAP(rsrc *as.Resource) []ldap.PartialAttribute {
ale's avatar
ale committed
220
221
222
223
224
225
226
227
	return []ldap.PartialAttribute{
		{Type: "objectClass", Vals: []string{"top", "mailingList"}},
		{Type: "listName", Vals: s2l(rsrc.ID.Name())},
		{Type: "public", Vals: s2l(b2s(rsrc.List.Public))},
		{Type: "listOwner", Vals: rsrc.List.Admins},
	}
}

228
229
func (h *mailingListResourceHandler) SearchQuery() *queryTemplate {
	return &queryTemplate{
ale's avatar
ale committed
230
231
		Base:   joinDN("ou=Lists", h.baseDN),
		Filter: "(&(objectClass=mailingList)(listName=${resource}))",
232
233
		Scope:  ldap.ScopeSingleLevel,
	}
ale's avatar
ale committed
234
235
236
237
238
239
240
}

// Website (subsite) resource.
type websiteResourceHandler struct {
	baseDN string
}

241
func (h *websiteResourceHandler) GetDN(id as.ResourceID) (string, error) {
ale's avatar
ale committed
242
243
244
245
246
247
248
249
250
251
252
	if id.User() == "" {
		return "", errors.New("unqualified resource id")
	}

	dn := replaceVars("alias=${resource},uid=${user},ou=People", map[string]string{
		"user":     id.User(),
		"resource": id.Name(),
	})
	return joinDN(dn, h.baseDN), nil
}

253
func (h *websiteResourceHandler) FromLDAP(entry *ldap.Entry) (*as.Resource, error) {
ale's avatar
ale committed
254
255
256
257
258
259
260
261
	if !isObjectClass(entry, "subSite") {
		return nil, errWrongObjectClass
	}

	alias := entry.GetAttributeValue("alias")
	parentSite := entry.GetAttributeValue("parentSite")
	name := fmt.Sprintf("%s/%s", parentSite, alias)
	url := fmt.Sprintf("https://www.%s/%s/", parentSite, alias)
262
	uid, _ := strconv.Atoi(entry.GetAttributeValue(uidNumberLDAPAttr)) // nolint
ale's avatar
ale committed
263
264
265
266
267

	username, err := getParentRDN(entry.DN, "uid")
	if err != nil {
		return nil, err
	}
268
269
270
	return &as.Resource{
		ID: as.NewResourceID(
			as.ResourceTypeWebsite,
ale's avatar
ale committed
271
272
273
			username,
			alias,
		),
274
		Type: as.ResourceTypeWebsite,
ale's avatar
ale committed
275
		Name: name,
276
		Website: &as.Website{
ale's avatar
ale committed
277
			URL:          url,
ale's avatar
ale committed
278
			UID:          uid,
ale's avatar
ale committed
279
280
281
282
283
284
285
286
			ParentDomain: parentSite,
			Options:      entry.GetAttributeValues("option"),
			DocumentRoot: entry.GetAttributeValue("documentRoot"),
			AcceptMail:   s2b(entry.GetAttributeValue("acceptMail")),
		},
	}, nil
}

287
func (h *websiteResourceHandler) ToLDAP(rsrc *as.Resource) []ldap.PartialAttribute {
ale's avatar
ale committed
288
289
290
291
292
293
294
295
296
297
	return []ldap.PartialAttribute{
		{Type: "objectClass", Vals: []string{"top", "subSite"}},
		{Type: "alias", Vals: s2l(rsrc.ID.Name())},
		{Type: "parentSite", Vals: s2l(rsrc.Website.ParentDomain)},
		{Type: "option", Vals: rsrc.Website.Options},
		{Type: "documentRoot", Vals: s2l(rsrc.Website.DocumentRoot)},
		{Type: "acceptMail", Vals: s2l(b2s(rsrc.Website.AcceptMail))},
	}
}

298
299
func (h *websiteResourceHandler) SearchQuery() *queryTemplate {
	return &queryTemplate{
ale's avatar
ale committed
300
301
		Base:   joinDN("ou=People", h.baseDN),
		Filter: "(&(objectClass=subSite)(alias=${resource}))",
302
303
		Scope:  ldap.ScopeWholeSubtree,
	}
ale's avatar
ale committed
304
305
306
307
308
309
310
}

// Domain (virtual host) resource.
type domainResourceHandler struct {
	baseDN string
}

311
func (h *domainResourceHandler) GetDN(id as.ResourceID) (string, error) {
ale's avatar
ale committed
312
313
314
315
316
317
318
319
320
321
322
	if id.User() == "" {
		return "", errors.New("unqualified resource id")
	}

	dn := replaceVars("cn=${resource},uid=${user},ou=People", map[string]string{
		"user":     id.User(),
		"resource": id.Name(),
	})
	return joinDN(dn, h.baseDN), nil
}

323
func (h *domainResourceHandler) FromLDAP(entry *ldap.Entry) (*as.Resource, error) {
ale's avatar
ale committed
324
325
326
327
328
329
330
331
332
	if !isObjectClass(entry, "virtualHost") {
		return nil, errWrongObjectClass
	}

	cn := entry.GetAttributeValue("cn")
	username, err := getParentRDN(entry.DN, "uid")
	if err != nil {
		return nil, err
	}
333
	uid, _ := strconv.Atoi(entry.GetAttributeValue(uidNumberLDAPAttr)) // nolint
ale's avatar
ale committed
334

335
336
337
	return &as.Resource{
		ID: as.NewResourceID(
			as.ResourceTypeDomain,
ale's avatar
ale committed
338
339
340
			username,
			cn,
		),
341
		Type: as.ResourceTypeDomain,
ale's avatar
ale committed
342
		Name: cn,
343
		Website: &as.Website{
ale's avatar
ale committed
344
			URL:          fmt.Sprintf("https://%s/", cn),
ale's avatar
ale committed
345
			UID:          uid,
ale's avatar
ale committed
346
347
348
349
350
351
352
			Options:      entry.GetAttributeValues("option"),
			DocumentRoot: entry.GetAttributeValue("documentRoot"),
			AcceptMail:   s2b(entry.GetAttributeValue("acceptMail")),
		},
	}, nil
}

353
func (h *domainResourceHandler) ToLDAP(rsrc *as.Resource) []ldap.PartialAttribute {
ale's avatar
ale committed
354
355
356
357
358
359
360
361
362
	return []ldap.PartialAttribute{
		{Type: "objectClass", Vals: []string{"top", "virtualHost"}},
		{Type: "cn", Vals: s2l(rsrc.ID.Name())},
		{Type: "option", Vals: rsrc.Website.Options},
		{Type: "documentRoot", Vals: s2l(rsrc.Website.DocumentRoot)},
		{Type: "acceptMail", Vals: s2l(b2s(rsrc.Website.AcceptMail))},
	}
}

363
364
func (h *domainResourceHandler) SearchQuery() *queryTemplate {
	return &queryTemplate{
ale's avatar
ale committed
365
366
		Base:   joinDN("ou=People", h.baseDN),
		Filter: "(&(objectClass=virtualHost)(cn=${resource}))",
367
368
		Scope:  ldap.ScopeWholeSubtree,
	}
ale's avatar
ale committed
369
370
371
372
373
374
375
}

// WebDAV (a.k.a. "ftp account") resource.
type webdavResourceHandler struct {
	baseDN string
}

376
func (h *webdavResourceHandler) GetDN(id as.ResourceID) (string, error) {
ale's avatar
ale committed
377
378
379
380
	if id.User() == "" {
		return "", errors.New("unqualified resource id")
	}

381
	dn := replaceVars("ftpname=${resource},uid=${user},ou=People", map[string]string{
ale's avatar
ale committed
382
383
384
385
386
387
		"user":     id.User(),
		"resource": id.Name(),
	})
	return joinDN(dn, h.baseDN), nil
}

388
func (h *webdavResourceHandler) FromLDAP(entry *ldap.Entry) (*as.Resource, error) {
ale's avatar
ale committed
389
390
391
392
393
394
395
396
397
	if !isObjectClass(entry, "ftpAccount") {
		return nil, errWrongObjectClass
	}

	name := entry.GetAttributeValue("ftpname")
	username, err := getParentRDN(entry.DN, "uid")
	if err != nil {
		return nil, err
	}
398
399
400
401
	uid, _ := strconv.Atoi(entry.GetAttributeValue(uidNumberLDAPAttr)) // nolint
	return &as.Resource{
		ID: as.NewResourceID(
			as.ResourceTypeDAV,
ale's avatar
ale committed
402
403
404
			username,
			name,
		),
405
		Type: as.ResourceTypeDAV,
ale's avatar
ale committed
406
		Name: name,
407
		DAV: &as.WebDAV{
ale's avatar
ale committed
408
			Homedir: entry.GetAttributeValue("homeDirectory"),
ale's avatar
ale committed
409
			UID:     uid,
ale's avatar
ale committed
410
411
412
413
		},
	}, nil
}

414
func (h *webdavResourceHandler) ToLDAP(rsrc *as.Resource) []ldap.PartialAttribute {
ale's avatar
ale committed
415
416
417
418
419
420
421
	return []ldap.PartialAttribute{
		{Type: "objectClass", Vals: []string{"top", "person", "posixAccount", "shadowAccount", "organizationalPerson", "inetOrgPerson", "ftpAccount"}},
		{Type: "ftpname", Vals: s2l(rsrc.ID.Name())},
		{Type: "homeDirectory", Vals: s2l(rsrc.DAV.Homedir)},
	}
}

422
423
func (h *webdavResourceHandler) SearchQuery() *queryTemplate {
	return &queryTemplate{
ale's avatar
ale committed
424
425
		Base:   joinDN("ou=People", h.baseDN),
		Filter: "(&(objectClass=ftpAccount)(ftpname=${resource}))",
426
427
		Scope:  ldap.ScopeWholeSubtree,
	}
ale's avatar
ale committed
428
429
430
431
432
433
434
435
436
437
438
439
440
}

// Databases are special: in LDAP, they encode their relation with a
// website using the database hierarchy. This means that, in order to
// satisfy the requirement to generate a DN directly from every
// resource ID, we need to encode the parent website in the resource
// ID itself. We do this using not just the website name (which would
// be ambiguous: Website or Domain?), but also the type, using an
// attr=name syntax.
type databaseResourceHandler struct {
	baseDN string
}

441
func makeDatabaseResourceID(dn string) (rsrcID, parentID as.ResourceID, err error) {
ale's avatar
ale committed
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
	parsed, perr := ldap.ParseDN(dn)
	if perr != nil {
		err = perr
		return
	}
	if len(parsed.RDNs) < 3 {
		err = errors.New("not enough DN components for database")
		return
	}

	// The database name is the first RDN.
	dbname := parsed.RDNs[0].Attributes[0].Value
	// The encoded parent website name is type=value of the 2nd component.
	parentName := parsed.RDNs[1].Attributes[0].Value
	encParent := fmt.Sprintf("%s=%s", parsed.RDNs[1].Attributes[0].Type, parentName)
	// The username is the 3rd component.
	username := parsed.RDNs[2].Attributes[0].Value

460
461
	rsrcID = as.NewResourceID(
		as.ResourceTypeDatabase,
ale's avatar
ale committed
462
463
464
465
		username,
		encParent,
		dbname,
	)
466
	var parentType = as.ResourceTypeWebsite
ale's avatar
ale committed
467
	if parsed.RDNs[1].Attributes[0].Type == "cn" {
468
		parentType = as.ResourceTypeDomain
ale's avatar
ale committed
469
	}
470
	parentID = as.NewResourceID(
ale's avatar
ale committed
471
472
473
474
475
476
477
		parentType,
		username,
		parentName,
	)
	return
}

478
func (h *databaseResourceHandler) GetDN(id as.ResourceID) (string, error) {
ale's avatar
ale committed
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
	if id.User() == "" || len(id.Parts) < 4 {
		return "", errors.New("unqualified resource id")
	}

	// Decode the parent website as encoded in
	// makeDatabaseResourceID. The parent website is the third
	// path component in the ID.
	parentParts := strings.SplitN(id.Parts[2], "=", 2)
	if len(parentParts) != 2 {
		return "", errors.New("malformed database resource id")
	}

	dn := replaceVars("dbname=${resource},${parentType}=${parent},uid=${user},ou=People", map[string]string{
		"user":       id.User(),
		"parentType": parentParts[0],
		"parent":     parentParts[1],
		"resource":   id.Name(),
	})
	return joinDN(dn, h.baseDN), nil
}

500
func (h *databaseResourceHandler) FromLDAP(entry *ldap.Entry) (*as.Resource, error) {
ale's avatar
ale committed
501
502
503
504
505
506
507
508
509
	if !isObjectClass(entry, "dbMysql") {
		return nil, errWrongObjectClass
	}

	name := entry.GetAttributeValue("dbname")
	rsrcID, parentID, err := makeDatabaseResourceID(entry.DN)
	if err != nil {
		return nil, err
	}
510
	return &as.Resource{
ale's avatar
ale committed
511
		ID:       rsrcID,
512
		Type:     as.ResourceTypeDatabase,
ale's avatar
ale committed
513
514
		ParentID: parentID,
		Name:     name,
515
		Database: &as.Database{
ale's avatar
ale committed
516
517
518
519
520
521
			DBUser:            entry.GetAttributeValue("dbuser"),
			CleartextPassword: entry.GetAttributeValue("clearPassword"),
		},
	}, nil
}

522
func (h *databaseResourceHandler) ToLDAP(rsrc *as.Resource) []ldap.PartialAttribute {
ale's avatar
ale committed
523
524
525
526
527
528
529
530
	return []ldap.PartialAttribute{
		{Type: "objectClass", Vals: []string{"top", "dbMysql"}},
		{Type: "dbname", Vals: s2l(rsrc.ID.Name())},
		{Type: "dbuser", Vals: s2l(rsrc.Database.DBUser)},
		{Type: "clearPassword", Vals: s2l(rsrc.Database.CleartextPassword)},
	}
}

531
532
func (h *databaseResourceHandler) SearchQuery() *queryTemplate {
	return &queryTemplate{
ale's avatar
ale committed
533
534
		Base:   joinDN("ou=People", h.baseDN),
		Filter: "(&(objectClass=dbMysql)(dbname=${resource}))",
535
536
		Scope:  ldap.ScopeWholeSubtree,
	}
ale's avatar
ale committed
537
538
539
540
541
}

func joinDN(parts ...string) string {
	return strings.Join(parts, ",")
}