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

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

9
	"github.com/go-ldap/ldap/v3"
ale's avatar
ale committed
10

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 {
ale's avatar
ale committed
17
18
	MakeDN(*as.User, *as.Resource) (string, error)
	GetOwner(*as.Resource) string
19
20
	ToLDAP(*as.Resource) []ldap.PartialAttribute
	FromLDAP(*ldap.Entry) (*as.Resource, error)
21
	SearchQuery() *queryTemplate
ale's avatar
ale committed
22
23
24
25
26
27
}

// Registry for demultiplexing resource handling. Has a similar
// interface to a resourceHandler, with a few exceptions.
type resourceRegistry struct {
	handlers map[string]resourceHandler
ale's avatar
ale committed
28
	types    []string
ale's avatar
ale committed
29
30
31
32
33
34
35
36
37
38
}

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

func (reg *resourceRegistry) register(rtype string, h resourceHandler) {
	reg.handlers[rtype] = h
ale's avatar
ale committed
39
	reg.types = append(reg.types, rtype)
ale's avatar
ale committed
40
41
42
43
44
45
46
47
48
49
}

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

ale's avatar
ale committed
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
func (reg *resourceRegistry) GetDN(rid as.ResourceID) (string, error) {
	return string(rid), nil
}

func (reg *resourceRegistry) GetOwner(rsrc *as.Resource) (username string) {
	// Error results in no owner.
	// nolint
	reg.dispatch(rsrc.Type, func(h resourceHandler) error {
		username = h.GetOwner(rsrc)
		return nil
	})
	return
}

func (reg *resourceRegistry) MakeDN(user *as.User, rsrc *as.Resource) (dn string, err error) {
	err = reg.dispatch(rsrc.Type, func(h resourceHandler) (dnerr error) {
		dn, dnerr = h.MakeDN(user, rsrc)
ale's avatar
ale committed
67
68
69
70
71
		return
	})
	return
}

72
func (reg *resourceRegistry) ToLDAP(rsrc *as.Resource) (attrs []ldap.PartialAttribute) {
ale's avatar
ale committed
73
	if err := reg.dispatch(rsrc.Type, func(h resourceHandler) error {
ale's avatar
ale committed
74
75
76
77
78
79
		attrs = h.ToLDAP(rsrc)
		return nil
	}); err != nil {
		return nil
	}

ale's avatar
ale committed
80
81
82
83
84
	if rsrc.CreatedAt != "" {
		attrs = append(attrs, ldap.PartialAttribute{
			Type: creationDateLDAPAttr, Vals: s2l(rsrc.CreatedAt)})
	}

ale's avatar
ale committed
85
86
87
88
89
90
91
92
	attrs = append(attrs, []ldap.PartialAttribute{
		{Type: "status", Vals: s2l(rsrc.Status)},
		{Type: "host", Vals: s2l(rsrc.Shard)},
		{Type: "originalHost", Vals: s2l(rsrc.OriginalShard)},
	}...)
	return
}

93
func setCommonResourceAttrs(entry *ldap.Entry, rsrc *as.Resource) {
ale's avatar
ale committed
94
95
96
	rsrc.Status = entry.GetAttributeValue("status")
	rsrc.Shard = entry.GetAttributeValue("host")
	rsrc.OriginalShard = entry.GetAttributeValue("originalHost")
ale's avatar
ale committed
97
	rsrc.CreatedAt = entry.GetAttributeValue(creationDateLDAPAttr)
ale's avatar
ale committed
98
99
}

100
func (reg *resourceRegistry) FromLDAP(entry *ldap.Entry) (rsrc *as.Resource, err error) {
ale's avatar
ale committed
101
102
	// Since we don't know what resource type to expect, we try
	// all known handlers until one returns a valid Resource.
103
104
	// This expects that all object types can be told apart by
	// their DN or attributes.
ale's avatar
ale committed
105
106
107
108
109
110
111
112
113
114
	for _, h := range reg.handlers {
		rsrc, err = h.FromLDAP(entry)
		if err == nil {
			setCommonResourceAttrs(entry, rsrc)
			return
		}
	}
	return nil, errors.New("unknown resource")
}

115
func (reg *resourceRegistry) SearchQuery(rsrcType string) (q *queryTemplate, err error) {
ale's avatar
ale committed
116
	err = reg.dispatch(rsrcType, func(h resourceHandler) error {
117
		q = h.SearchQuery()
ale's avatar
ale committed
118
119
120
121
122
123
124
125
126
127
		return nil
	})
	return
}

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

128
129
130
131
var (
	errWrongObjectClass = errors.New("objectClass does not match")
	errWrongSubtree     = errors.New("subtree does not match")
)
ale's avatar
ale committed
132
133
134
135

func (h *emailResourceHandler) MakeDN(user *as.User, rsrc *as.Resource) (string, error) {
	if user == nil {
		return "", errors.New("email resource requires an owner")
ale's avatar
ale committed
136
	}
ale's avatar
ale committed
137
	rdn := replaceVars("mail=${resource}", templateVars{"resource": rsrc.Name})
ale's avatar
ale committed
138
	return joinDN(rdn, getUserDN(user, h.baseDN)), nil
ale's avatar
ale committed
139
140
}

ale's avatar
ale committed
141
142
143
func (h *emailResourceHandler) GetOwner(rsrc *as.Resource) string {
	return ownerFromDN(rsrc.ID.String())
}
ale's avatar
ale committed
144

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

	email := entry.GetAttributeValue("mail")

152
	return &as.Resource{
ale's avatar
ale committed
153
		ID:   as.ResourceID(entry.DN),
154
		Type: as.ResourceTypeEmail,
ale's avatar
ale committed
155
		Name: email,
156
		Email: &as.Email{
ale's avatar
ale committed
157
			Aliases: entry.GetAttributeValues("mailAlternateAddress"),
ale's avatar
ale committed
158
159
160
161
162
			Maildir: entry.GetAttributeValue("mailMessageStore"),
		},
	}, nil
}

163
func (h *emailResourceHandler) ToLDAP(rsrc *as.Resource) []ldap.PartialAttribute {
ale's avatar
ale committed
164
165
	return []ldap.PartialAttribute{
		{Type: "objectClass", Vals: []string{"top", "virtualMailUser"}},
ale's avatar
ale committed
166
		{Type: "mail", Vals: s2l(rsrc.Name)},
ale's avatar
ale committed
167
		{Type: "mailAlternateAddress", Vals: rsrc.Email.Aliases},
ale's avatar
ale committed
168
169
170
171
		{Type: "mailMessageStore", Vals: s2l(rsrc.Email.Maildir)},
	}
}

172
173
func (h *emailResourceHandler) SearchQuery() *queryTemplate {
	return &queryTemplate{
ale's avatar
ale committed
174
		Base:   joinDN("ou=People", h.baseDN),
ale's avatar
ale committed
175
		Filter: "(&(objectClass=virtualMailUser)(|(mail=${resource})(mailAlternateAddress=${resource})))",
176
177
		Scope:  ldap.ScopeWholeSubtree,
	}
ale's avatar
ale committed
178
179
}

180
// Mailing list resource. They are stored on a separate LDAP subtree.
ale's avatar
ale committed
181
182
type mailingListResourceHandler struct {
	baseDN string
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197

	// Keep around a parsed DN for the ou=Lists root, so we can
	// easily call DN.AncestorOf in FromLDAP later.
	listsDN *ldap.DN
}

func newMailingListResourceHandler(baseDN string) *mailingListResourceHandler {
	dn, err := ldap.ParseDN(joinDN("ou=Lists", baseDN))
	if err != nil {
		panic(err)
	}
	return &mailingListResourceHandler{
		baseDN:  baseDN,
		listsDN: dn,
	}
ale's avatar
ale committed
198
199
}

ale's avatar
ale committed
200
func (h *mailingListResourceHandler) MakeDN(_ *as.User, rsrc *as.Resource) (string, error) {
ale's avatar
ale committed
201
	rdn := replaceVars("listName=${resource}", templateVars{"resource": rsrc.Name})
ale's avatar
ale committed
202
203
204
205
206
207
	return joinDN(rdn, "ou=Lists", h.baseDN), nil
}

func (h *mailingListResourceHandler) GetOwner(rsrc *as.Resource) string {
	// No exclusive owners for mailing lists.
	return ""
ale's avatar
ale committed
208
209
}

210
func (h *mailingListResourceHandler) FromLDAP(entry *ldap.Entry) (*as.Resource, error) {
211
212
	// Match on objectClass and subtree (the mailingList
	// objectClass is also used by newsletters).
ale's avatar
ale committed
213
214
215
	if !isObjectClass(entry, "mailingList") {
		return nil, errWrongObjectClass
	}
216
217
218
219
220
221
222
	dn, err := ldap.ParseDN(entry.DN)
	if err != nil {
		panic(err)
	}
	if !h.listsDN.AncestorOf(dn) {
		return nil, errWrongSubtree
	}
ale's avatar
ale committed
223
224

	listName := entry.GetAttributeValue("listName")
225
	return &as.Resource{
ale's avatar
ale committed
226
		ID:   as.ResourceID(entry.DN),
227
		Type: as.ResourceTypeMailingList,
ale's avatar
ale committed
228
		Name: listName,
229
		List: &as.MailingList{
ale's avatar
ale committed
230
231
232
233
234
235
			Public: s2b(entry.GetAttributeValue("public")),
			Admins: entry.GetAttributeValues("listOwner"),
		},
	}, nil
}

236
func (h *mailingListResourceHandler) ToLDAP(rsrc *as.Resource) []ldap.PartialAttribute {
ale's avatar
ale committed
237
238
	return []ldap.PartialAttribute{
		{Type: "objectClass", Vals: []string{"top", "mailingList"}},
ale's avatar
ale committed
239
		{Type: "listName", Vals: s2l(rsrc.Name)},
ale's avatar
ale committed
240
241
242
243
244
		{Type: "public", Vals: s2l(b2s(rsrc.List.Public))},
		{Type: "listOwner", Vals: rsrc.List.Admins},
	}
}

245
246
func (h *mailingListResourceHandler) SearchQuery() *queryTemplate {
	return &queryTemplate{
ale's avatar
ale committed
247
248
		Base:   joinDN("ou=Lists", h.baseDN),
		Filter: "(&(objectClass=mailingList)(listName=${resource}))",
249
250
		Scope:  ldap.ScopeSingleLevel,
	}
ale's avatar
ale committed
251
252
}

253
254
// Newsletter resource. Like mailing lists, these are stored on their
// own separate subtree.
putro's avatar
putro committed
255
256
type newsletterResourceHandler struct {
	baseDN string
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271

	// Keep around a parsed DN for the ou=Newsletters root, so we can
	// easily call DN.AncestorOf in FromLDAP later.
	newslettersDN *ldap.DN
}

func newNewsletterResourceHandler(baseDN string) *newsletterResourceHandler {
	dn, err := ldap.ParseDN(joinDN("ou=Newsletters", baseDN))
	if err != nil {
		panic(err)
	}
	return &newsletterResourceHandler{
		baseDN:        baseDN,
		newslettersDN: dn,
	}
putro's avatar
putro committed
272
273
274
275
276
277
278
279
280
281
282
283
284
}

func (h *newsletterResourceHandler) MakeDN(_ *as.User, rsrc *as.Resource) (string, error) {
	rdn := replaceVars("listName=${resource}", templateVars{"resource": rsrc.Name})
	return joinDN(rdn, "ou=Newsletters", h.baseDN), nil
}

func (h *newsletterResourceHandler) GetOwner(rsrc *as.Resource) string {
	// No exclusive owners for newsletters.
	return ""
}

func (h *newsletterResourceHandler) FromLDAP(entry *ldap.Entry) (*as.Resource, error) {
285
	// Match on objectClass and subtree.
putro's avatar
putro committed
286
287
288
	if !isObjectClass(entry, "mailingList") {
		return nil, errWrongObjectClass
	}
289
290
291
292
293
294
295
	dn, err := ldap.ParseDN(entry.DN)
	if err != nil {
		panic(err)
	}
	if !h.newslettersDN.AncestorOf(dn) {
		return nil, errWrongSubtree
	}
putro's avatar
putro committed
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311

	listName := entry.GetAttributeValue("listName")
	return &as.Resource{
		ID:   as.ResourceID(entry.DN),
		Type: as.ResourceTypeNewsletter,
		Name: listName,
		Newsletter: &as.Newsletter{
			Admins: entry.GetAttributeValues("listOwner"),
		},
	}, nil
}

func (h *newsletterResourceHandler) ToLDAP(rsrc *as.Resource) []ldap.PartialAttribute {
	return []ldap.PartialAttribute{
		{Type: "objectClass", Vals: []string{"top", "mailingList"}},
		{Type: "listName", Vals: s2l(rsrc.Name)},
312
313
		// TODO: check if public is actually mandated by the schema.
		{Type: "public", Vals: []string{"yes"}},
putro's avatar
putro committed
314
315
316
317
318
319
320
321
322
323
324
325
		{Type: "listOwner", Vals: rsrc.Newsletter.Admins},
	}
}

func (h *newsletterResourceHandler) SearchQuery() *queryTemplate {
	return &queryTemplate{
		Base:   joinDN("ou=Newsletters", h.baseDN),
		Filter: "(&(objectClass=mailingList)(listName=${resource}))",
		Scope:  ldap.ScopeSingleLevel,
	}
}

ale's avatar
ale committed
326
327
328
329
330
// Website (subsite) resource.
type websiteResourceHandler struct {
	baseDN string
}

ale's avatar
ale committed
331
332
333
func (h *websiteResourceHandler) MakeDN(user *as.User, rsrc *as.Resource) (string, error) {
	if user == nil {
		return "", errors.New("website resource requires an owner")
ale's avatar
ale committed
334
	}
ale's avatar
ale committed
335
	rdn := replaceVars("alias=${resource}", templateVars{"resource": rsrc.Name})
ale's avatar
ale committed
336
337
	return joinDN(rdn, getUserDN(user, h.baseDN)), nil
}
ale's avatar
ale committed
338

ale's avatar
ale committed
339
340
func (h *websiteResourceHandler) GetOwner(rsrc *as.Resource) string {
	return ownerFromDN(rsrc.ID.String())
ale's avatar
ale committed
341
342
}

343
func (h *websiteResourceHandler) FromLDAP(entry *ldap.Entry) (*as.Resource, error) {
ale's avatar
ale committed
344
345
346
347
348
349
	if !isObjectClass(entry, "subSite") {
		return nil, errWrongObjectClass
	}

	alias := entry.GetAttributeValue("alias")
	parentSite := entry.GetAttributeValue("parentSite")
ale's avatar
ale committed
350
	//name := fmt.Sprintf("%s/%s", parentSite, alias)
ale's avatar
ale committed
351
	url := fmt.Sprintf("https://www.%s/%s/", parentSite, alias)
352
	uid, _ := strconv.Atoi(entry.GetAttributeValue(uidNumberLDAPAttr)) // nolint
ale's avatar
ale committed
353
	statsID, _ := strconv.Atoi(entry.GetAttributeValue("statsId"))     // nolint
ale's avatar
ale committed
354

355
	return &as.Resource{
ale's avatar
ale committed
356
		ID:   as.ResourceID(entry.DN),
357
		Type: as.ResourceTypeWebsite,
ale's avatar
ale committed
358
		Name: alias,
359
		Website: &as.Website{
ale's avatar
ale committed
360
			URL:          url,
ale's avatar
ale committed
361
			UID:          uid,
ale's avatar
ale committed
362
363
364
			ParentDomain: parentSite,
			Options:      entry.GetAttributeValues("option"),
			DocumentRoot: entry.GetAttributeValue("documentRoot"),
ale's avatar
ale committed
365
			AcceptMail:   false,
ale's avatar
ale committed
366
			StatsID:      statsID,
ale's avatar
ale committed
367
368
369
370
		},
	}, nil
}

ale's avatar
ale committed
371
372
373
374
375
376
377
func nonZeroIntToList(i int) []string {
	if i == 0 {
		return nil
	}
	return []string{strconv.Itoa(i)}
}

378
func (h *websiteResourceHandler) ToLDAP(rsrc *as.Resource) []ldap.PartialAttribute {
ale's avatar
ale committed
379
	//bareName := strings.Split(rsrc.Name, "/")[0]
ale's avatar
ale committed
380
381
	return []ldap.PartialAttribute{
		{Type: "objectClass", Vals: []string{"top", "subSite"}},
ale's avatar
ale committed
382
		{Type: "alias", Vals: s2l(rsrc.Name)},
ale's avatar
ale committed
383
384
385
		{Type: "parentSite", Vals: s2l(rsrc.Website.ParentDomain)},
		{Type: "option", Vals: rsrc.Website.Options},
		{Type: "documentRoot", Vals: s2l(rsrc.Website.DocumentRoot)},
ale's avatar
ale committed
386
		{Type: uidNumberLDAPAttr, Vals: s2l(strconv.Itoa(rsrc.Website.UID))},
ale's avatar
ale committed
387
		{Type: "statsId", Vals: nonZeroIntToList(rsrc.Website.StatsID)},
ale's avatar
ale committed
388
389
390
	}
}

391
392
func (h *websiteResourceHandler) SearchQuery() *queryTemplate {
	return &queryTemplate{
ale's avatar
ale committed
393
394
		Base:   joinDN("ou=People", h.baseDN),
		Filter: "(&(objectClass=subSite)(alias=${resource}))",
395
396
		Scope:  ldap.ScopeWholeSubtree,
	}
ale's avatar
ale committed
397
398
399
400
401
402
403
}

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

ale's avatar
ale committed
404
405
406
func (h *domainResourceHandler) MakeDN(user *as.User, rsrc *as.Resource) (string, error) {
	if user == nil {
		return "", errors.New("domain resource requires an owner")
ale's avatar
ale committed
407
	}
ale's avatar
ale committed
408
	rdn := replaceVars("cn=${resource}", templateVars{"resource": rsrc.Name})
ale's avatar
ale committed
409
410
	return joinDN(rdn, getUserDN(user, h.baseDN)), nil
}
ale's avatar
ale committed
411

ale's avatar
ale committed
412
413
func (h *domainResourceHandler) GetOwner(rsrc *as.Resource) string {
	return ownerFromDN(rsrc.ID.String())
ale's avatar
ale committed
414
415
}

416
func (h *domainResourceHandler) FromLDAP(entry *ldap.Entry) (*as.Resource, error) {
ale's avatar
ale committed
417
418
419
420
421
	if !isObjectClass(entry, "virtualHost") {
		return nil, errWrongObjectClass
	}

	cn := entry.GetAttributeValue("cn")
422
	uid, _ := strconv.Atoi(entry.GetAttributeValue(uidNumberLDAPAttr)) // nolint
ale's avatar
ale committed
423
	statsID, _ := strconv.Atoi(entry.GetAttributeValue("statsId"))     // nolint
ale's avatar
ale committed
424

425
	return &as.Resource{
ale's avatar
ale committed
426
		ID:   as.ResourceID(entry.DN),
427
		Type: as.ResourceTypeDomain,
ale's avatar
ale committed
428
		Name: cn,
429
		Website: &as.Website{
ale's avatar
ale committed
430
			URL:          fmt.Sprintf("https://%s/", cn),
ale's avatar
ale committed
431
			UID:          uid,
ale's avatar
ale committed
432
433
434
			Options:      entry.GetAttributeValues("option"),
			DocumentRoot: entry.GetAttributeValue("documentRoot"),
			AcceptMail:   s2b(entry.GetAttributeValue("acceptMail")),
ale's avatar
ale committed
435
			StatsID:      statsID,
ale's avatar
ale committed
436
437
438
439
		},
	}, nil
}

440
func (h *domainResourceHandler) ToLDAP(rsrc *as.Resource) []ldap.PartialAttribute {
ale's avatar
ale committed
441
442
	return []ldap.PartialAttribute{
		{Type: "objectClass", Vals: []string{"top", "virtualHost"}},
ale's avatar
ale committed
443
		{Type: "cn", Vals: s2l(rsrc.Name)},
ale's avatar
ale committed
444
445
446
		{Type: "option", Vals: rsrc.Website.Options},
		{Type: "documentRoot", Vals: s2l(rsrc.Website.DocumentRoot)},
		{Type: "acceptMail", Vals: s2l(b2s(rsrc.Website.AcceptMail))},
ale's avatar
ale committed
447
		{Type: uidNumberLDAPAttr, Vals: s2l(strconv.Itoa(rsrc.Website.UID))},
ale's avatar
ale committed
448
		{Type: "statsId", Vals: nonZeroIntToList(rsrc.Website.StatsID)},
ale's avatar
ale committed
449
450
451
	}
}

452
453
func (h *domainResourceHandler) SearchQuery() *queryTemplate {
	return &queryTemplate{
ale's avatar
ale committed
454
455
		Base:   joinDN("ou=People", h.baseDN),
		Filter: "(&(objectClass=virtualHost)(cn=${resource}))",
456
457
		Scope:  ldap.ScopeWholeSubtree,
	}
ale's avatar
ale committed
458
459
460
461
462
463
464
}

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

ale's avatar
ale committed
465
466
467
func (h *webdavResourceHandler) MakeDN(user *as.User, rsrc *as.Resource) (string, error) {
	if user == nil {
		return "", errors.New("DAV resource requires an owner")
ale's avatar
ale committed
468
	}
ale's avatar
ale committed
469
	rdn := replaceVars("ftpname=${resource}", templateVars{"resource": rsrc.Name})
ale's avatar
ale committed
470
471
	return joinDN(rdn, getUserDN(user, h.baseDN)), nil
}
ale's avatar
ale committed
472

ale's avatar
ale committed
473
474
func (h *webdavResourceHandler) GetOwner(rsrc *as.Resource) string {
	return ownerFromDN(rsrc.ID.String())
ale's avatar
ale committed
475
476
}

477
func (h *webdavResourceHandler) FromLDAP(entry *ldap.Entry) (*as.Resource, error) {
ale's avatar
ale committed
478
479
480
481
482
	if !isObjectClass(entry, "ftpAccount") {
		return nil, errWrongObjectClass
	}

	name := entry.GetAttributeValue("ftpname")
483
484
	uid, _ := strconv.Atoi(entry.GetAttributeValue(uidNumberLDAPAttr)) // nolint
	return &as.Resource{
ale's avatar
ale committed
485
		ID:   as.ResourceID(entry.DN),
486
		Type: as.ResourceTypeDAV,
ale's avatar
ale committed
487
		Name: name,
488
		DAV: &as.WebDAV{
ale's avatar
ale committed
489
			Homedir: entry.GetAttributeValue("homeDirectory"),
ale's avatar
ale committed
490
			UID:     uid,
ale's avatar
ale committed
491
492
493
494
		},
	}, nil
}

495
func (h *webdavResourceHandler) ToLDAP(rsrc *as.Resource) []ldap.PartialAttribute {
ale's avatar
ale committed
496
	return []ldap.PartialAttribute{
497
498
499
		// Note: there is really no need to have inetOrgPerson here, but
		// we have to keep it for compatibility with our legacy schema.
		{Type: "objectClass", Vals: []string{"top", "inetOrgPerson", "posixAccount", "shadowAccount", "ftpAccount"}},
ale's avatar
ale committed
500
		{Type: "ftpname", Vals: s2l(rsrc.Name)},
ale's avatar
ale committed
501
		{Type: "homeDirectory", Vals: s2l(rsrc.DAV.Homedir)},
ale's avatar
ale committed
502
		{Type: uidNumberLDAPAttr, Vals: s2l(strconv.Itoa(rsrc.DAV.UID))},
ale's avatar
ale committed
503
504
505
506
		{Type: gidNumberLDAPAttr, Vals: s2l("2000")},
		{Type: "uid", Vals: s2l(rsrc.Name)},
		{Type: "cn", Vals: s2l(rsrc.Name)},
		{Type: "sn", Vals: s2l("Private")},
ale's avatar
ale committed
507
508
509
	}
}

510
511
func (h *webdavResourceHandler) SearchQuery() *queryTemplate {
	return &queryTemplate{
ale's avatar
ale committed
512
513
		Base:   joinDN("ou=People", h.baseDN),
		Filter: "(&(objectClass=ftpAccount)(ftpname=${resource}))",
514
515
		Scope:  ldap.ScopeWholeSubtree,
	}
ale's avatar
ale committed
516
517
518
519
520
521
522
523
524
525
526
527
528
}

// 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
}

ale's avatar
ale committed
529
func (h *databaseResourceHandler) MakeDN(user *as.User, rsrc *as.Resource) (string, error) {
ale's avatar
ale committed
530
	rdn := replaceVars("dbname=${resource}", templateVars{"resource": rsrc.Name})
ale's avatar
ale committed
531
	return joinDN(rdn, string(rsrc.ParentID)), nil
ale's avatar
ale committed
532
533
}

ale's avatar
ale committed
534
535
func (h *databaseResourceHandler) GetOwner(rsrc *as.Resource) string {
	return ownerFromDN(rsrc.ID.String())
ale's avatar
ale committed
536
537
}

538
func (h *databaseResourceHandler) FromLDAP(entry *ldap.Entry) (*as.Resource, error) {
ale's avatar
ale committed
539
540
541
542
543
	if !isObjectClass(entry, "dbMysql") {
		return nil, errWrongObjectClass
	}

	name := entry.GetAttributeValue("dbname")
ale's avatar
ale committed
544
545
546
547
548
549
550
551

	// Find the parent DN.
	var parentDN string
	if n := strings.IndexByte(entry.DN, ','); n > 0 {
		parentDN = entry.DN[n+1:]
	}
	if parentDN == "" {
		return nil, errors.New("can't determine parent DN")
ale's avatar
ale committed
552
	}
553
	return &as.Resource{
ale's avatar
ale committed
554
		ID:       as.ResourceID(entry.DN),
555
		Type:     as.ResourceTypeDatabase,
ale's avatar
ale committed
556
		ParentID: as.ResourceID(parentDN),
ale's avatar
ale committed
557
		Name:     name,
558
		Database: &as.Database{
559
			DBUser: entry.GetAttributeValue("dbuser"),
ale's avatar
ale committed
560
561
562
563
		},
	}, nil
}

564
func (h *databaseResourceHandler) ToLDAP(rsrc *as.Resource) []ldap.PartialAttribute {
ale's avatar
ale committed
565
566
	return []ldap.PartialAttribute{
		{Type: "objectClass", Vals: []string{"top", "dbMysql"}},
ale's avatar
ale committed
567
		{Type: "dbname", Vals: s2l(rsrc.Name)},
ale's avatar
ale committed
568
569
570
571
		{Type: "dbuser", Vals: s2l(rsrc.Database.DBUser)},
	}
}

572
573
func (h *databaseResourceHandler) SearchQuery() *queryTemplate {
	return &queryTemplate{
ale's avatar
ale committed
574
575
		Base:   joinDN("ou=People", h.baseDN),
		Filter: "(&(objectClass=dbMysql)(dbname=${resource}))",
576
577
		Scope:  ldap.ScopeWholeSubtree,
	}
ale's avatar
ale committed
578
579
}

ale's avatar
ale committed
580
581
582
func newDefaultResourceRegistry(baseDN string) *resourceRegistry {
	reg := newResourceRegistry()
	reg.register(as.ResourceTypeEmail, &emailResourceHandler{baseDN: baseDN})
583
584
	reg.register(as.ResourceTypeMailingList, newMailingListResourceHandler(baseDN))
	reg.register(as.ResourceTypeNewsletter, newNewsletterResourceHandler(baseDN))
ale's avatar
ale committed
585
586
587
588
589
590
	reg.register(as.ResourceTypeDAV, &webdavResourceHandler{baseDN: baseDN})
	reg.register(as.ResourceTypeWebsite, &websiteResourceHandler{baseDN: baseDN})
	reg.register(as.ResourceTypeDomain, &domainResourceHandler{baseDN: baseDN})
	reg.register(as.ResourceTypeDatabase, &databaseResourceHandler{baseDN: baseDN})
	return reg
}