resources.go 14.1 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 {
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 80 81 82 83 84 85 86 87
		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
}

88
func setCommonResourceAttrs(entry *ldap.Entry, rsrc *as.Resource) {
ale's avatar
ale committed
89 90 91 92 93
	rsrc.Status = entry.GetAttributeValue("status")
	rsrc.Shard = entry.GetAttributeValue("host")
	rsrc.OriginalShard = entry.GetAttributeValue("originalHost")
}

94
func (reg *resourceRegistry) FromLDAP(entry *ldap.Entry) (rsrc *as.Resource, err error) {
ale's avatar
ale committed
95 96 97 98 99 100 101 102 103 104 105 106
	// 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")
}

107
func (reg *resourceRegistry) SearchQuery(rsrcType string) (q *queryTemplate, err error) {
ale's avatar
ale committed
108
	err = reg.dispatch(rsrcType, func(h resourceHandler) error {
109
		q = h.SearchQuery()
ale's avatar
ale committed
110 111 112 113 114 115 116 117 118 119
		return nil
	})
	return
}

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

ale's avatar
ale committed
120 121 122 123 124
var errWrongObjectClass = errors.New("objectClass does not match")

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
125
	}
ale's avatar
ale committed
126
	rdn := replaceVars("mail=${resource}", templateVars{"resource": rsrc.Name})
ale's avatar
ale committed
127
	return joinDN(rdn, getUserDN(user, h.baseDN)), nil
ale's avatar
ale committed
128 129
}

ale's avatar
ale committed
130 131 132
func (h *emailResourceHandler) GetOwner(rsrc *as.Resource) string {
	return ownerFromDN(rsrc.ID.String())
}
ale's avatar
ale committed
133

134
func (h *emailResourceHandler) FromLDAP(entry *ldap.Entry) (*as.Resource, error) {
ale's avatar
ale committed
135 136 137 138 139 140
	if !isObjectClass(entry, "virtualMailUser") {
		return nil, errWrongObjectClass
	}

	email := entry.GetAttributeValue("mail")

141
	return &as.Resource{
ale's avatar
ale committed
142
		ID:   as.ResourceID(entry.DN),
143
		Type: as.ResourceTypeEmail,
ale's avatar
ale committed
144
		Name: email,
145
		Email: &as.Email{
ale's avatar
ale committed
146
			Aliases: entry.GetAttributeValues("mailAlternateAddress"),
ale's avatar
ale committed
147 148 149 150 151
			Maildir: entry.GetAttributeValue("mailMessageStore"),
		},
	}, nil
}

152
func (h *emailResourceHandler) ToLDAP(rsrc *as.Resource) []ldap.PartialAttribute {
ale's avatar
ale committed
153 154
	return []ldap.PartialAttribute{
		{Type: "objectClass", Vals: []string{"top", "virtualMailUser"}},
ale's avatar
ale committed
155
		{Type: "mail", Vals: s2l(rsrc.Name)},
ale's avatar
ale committed
156
		{Type: "mailAlternateAddress", Vals: rsrc.Email.Aliases},
ale's avatar
ale committed
157 158 159 160
		{Type: "mailMessageStore", Vals: s2l(rsrc.Email.Maildir)},
	}
}

161 162
func (h *emailResourceHandler) SearchQuery() *queryTemplate {
	return &queryTemplate{
ale's avatar
ale committed
163
		Base:   joinDN("ou=People", h.baseDN),
ale's avatar
ale committed
164
		Filter: "(&(objectClass=virtualMailUser)(|(mail=${resource})(mailAlternateAddress=${resource})))",
165 166
		Scope:  ldap.ScopeWholeSubtree,
	}
ale's avatar
ale committed
167 168 169 170 171 172 173
}

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

ale's avatar
ale committed
174
func (h *mailingListResourceHandler) MakeDN(_ *as.User, rsrc *as.Resource) (string, error) {
ale's avatar
ale committed
175
	rdn := replaceVars("listName=${resource}", templateVars{"resource": rsrc.Name})
ale's avatar
ale committed
176 177 178 179 180 181
	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
182 183
}

184
func (h *mailingListResourceHandler) FromLDAP(entry *ldap.Entry) (*as.Resource, error) {
ale's avatar
ale committed
185 186 187 188 189
	if !isObjectClass(entry, "mailingList") {
		return nil, errWrongObjectClass
	}

	listName := entry.GetAttributeValue("listName")
190
	return &as.Resource{
ale's avatar
ale committed
191
		ID:   as.ResourceID(entry.DN),
192
		Type: as.ResourceTypeMailingList,
ale's avatar
ale committed
193
		Name: listName,
194
		List: &as.MailingList{
ale's avatar
ale committed
195 196 197 198 199 200
			Public: s2b(entry.GetAttributeValue("public")),
			Admins: entry.GetAttributeValues("listOwner"),
		},
	}, nil
}

201
func (h *mailingListResourceHandler) ToLDAP(rsrc *as.Resource) []ldap.PartialAttribute {
ale's avatar
ale committed
202 203
	return []ldap.PartialAttribute{
		{Type: "objectClass", Vals: []string{"top", "mailingList"}},
ale's avatar
ale committed
204
		{Type: "listName", Vals: s2l(rsrc.Name)},
ale's avatar
ale committed
205 206 207 208 209
		{Type: "public", Vals: s2l(b2s(rsrc.List.Public))},
		{Type: "listOwner", Vals: rsrc.List.Admins},
	}
}

210 211
func (h *mailingListResourceHandler) SearchQuery() *queryTemplate {
	return &queryTemplate{
ale's avatar
ale committed
212 213
		Base:   joinDN("ou=Lists", h.baseDN),
		Filter: "(&(objectClass=mailingList)(listName=${resource}))",
214 215
		Scope:  ldap.ScopeSingleLevel,
	}
ale's avatar
ale committed
216 217 218 219 220 221 222
}

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

ale's avatar
ale committed
223 224 225
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
226
	}
ale's avatar
ale committed
227
	rdn := replaceVars("alias=${resource}", templateVars{"resource": rsrc.Name})
ale's avatar
ale committed
228 229
	return joinDN(rdn, getUserDN(user, h.baseDN)), nil
}
ale's avatar
ale committed
230

ale's avatar
ale committed
231 232
func (h *websiteResourceHandler) GetOwner(rsrc *as.Resource) string {
	return ownerFromDN(rsrc.ID.String())
ale's avatar
ale committed
233 234
}

235
func (h *websiteResourceHandler) FromLDAP(entry *ldap.Entry) (*as.Resource, error) {
ale's avatar
ale committed
236 237 238 239 240 241
	if !isObjectClass(entry, "subSite") {
		return nil, errWrongObjectClass
	}

	alias := entry.GetAttributeValue("alias")
	parentSite := entry.GetAttributeValue("parentSite")
ale's avatar
ale committed
242
	//name := fmt.Sprintf("%s/%s", parentSite, alias)
ale's avatar
ale committed
243
	url := fmt.Sprintf("https://www.%s/%s/", parentSite, alias)
244
	uid, _ := strconv.Atoi(entry.GetAttributeValue(uidNumberLDAPAttr)) // nolint
ale's avatar
ale committed
245

246
	return &as.Resource{
ale's avatar
ale committed
247
		ID:   as.ResourceID(entry.DN),
248
		Type: as.ResourceTypeWebsite,
ale's avatar
ale committed
249
		Name: alias,
250
		Website: &as.Website{
ale's avatar
ale committed
251
			URL:          url,
ale's avatar
ale committed
252
			UID:          uid,
ale's avatar
ale committed
253 254 255 256 257 258 259 260
			ParentDomain: parentSite,
			Options:      entry.GetAttributeValues("option"),
			DocumentRoot: entry.GetAttributeValue("documentRoot"),
			AcceptMail:   s2b(entry.GetAttributeValue("acceptMail")),
		},
	}, nil
}

261
func (h *websiteResourceHandler) ToLDAP(rsrc *as.Resource) []ldap.PartialAttribute {
ale's avatar
ale committed
262
	//bareName := strings.Split(rsrc.Name, "/")[0]
ale's avatar
ale committed
263 264
	return []ldap.PartialAttribute{
		{Type: "objectClass", Vals: []string{"top", "subSite"}},
ale's avatar
ale committed
265
		{Type: "alias", Vals: s2l(rsrc.Name)},
ale's avatar
ale committed
266 267 268 269
		{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))},
ale's avatar
ale committed
270
		{Type: uidNumberLDAPAttr, Vals: s2l(strconv.Itoa(rsrc.Website.UID))},
ale's avatar
ale committed
271 272 273
	}
}

274 275
func (h *websiteResourceHandler) SearchQuery() *queryTemplate {
	return &queryTemplate{
ale's avatar
ale committed
276 277
		Base:   joinDN("ou=People", h.baseDN),
		Filter: "(&(objectClass=subSite)(alias=${resource}))",
278 279
		Scope:  ldap.ScopeWholeSubtree,
	}
ale's avatar
ale committed
280 281 282 283 284 285 286
}

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

ale's avatar
ale committed
287 288 289
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
290
	}
ale's avatar
ale committed
291
	rdn := replaceVars("cn=${resource}", templateVars{"resource": rsrc.Name})
ale's avatar
ale committed
292 293
	return joinDN(rdn, getUserDN(user, h.baseDN)), nil
}
ale's avatar
ale committed
294

ale's avatar
ale committed
295 296
func (h *domainResourceHandler) GetOwner(rsrc *as.Resource) string {
	return ownerFromDN(rsrc.ID.String())
ale's avatar
ale committed
297 298
}

299
func (h *domainResourceHandler) FromLDAP(entry *ldap.Entry) (*as.Resource, error) {
ale's avatar
ale committed
300 301 302 303 304
	if !isObjectClass(entry, "virtualHost") {
		return nil, errWrongObjectClass
	}

	cn := entry.GetAttributeValue("cn")
305
	uid, _ := strconv.Atoi(entry.GetAttributeValue(uidNumberLDAPAttr)) // nolint
ale's avatar
ale committed
306

307
	return &as.Resource{
ale's avatar
ale committed
308
		ID:   as.ResourceID(entry.DN),
309
		Type: as.ResourceTypeDomain,
ale's avatar
ale committed
310
		Name: cn,
311
		Website: &as.Website{
ale's avatar
ale committed
312
			URL:          fmt.Sprintf("https://%s/", cn),
ale's avatar
ale committed
313
			UID:          uid,
ale's avatar
ale committed
314 315 316 317 318 319 320
			Options:      entry.GetAttributeValues("option"),
			DocumentRoot: entry.GetAttributeValue("documentRoot"),
			AcceptMail:   s2b(entry.GetAttributeValue("acceptMail")),
		},
	}, nil
}

321
func (h *domainResourceHandler) ToLDAP(rsrc *as.Resource) []ldap.PartialAttribute {
ale's avatar
ale committed
322 323
	return []ldap.PartialAttribute{
		{Type: "objectClass", Vals: []string{"top", "virtualHost"}},
ale's avatar
ale committed
324
		{Type: "cn", Vals: s2l(rsrc.Name)},
ale's avatar
ale committed
325 326 327
		{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
328
		{Type: uidNumberLDAPAttr, Vals: s2l(strconv.Itoa(rsrc.Website.UID))},
ale's avatar
ale committed
329 330 331
	}
}

332 333
func (h *domainResourceHandler) SearchQuery() *queryTemplate {
	return &queryTemplate{
ale's avatar
ale committed
334 335
		Base:   joinDN("ou=People", h.baseDN),
		Filter: "(&(objectClass=virtualHost)(cn=${resource}))",
336 337
		Scope:  ldap.ScopeWholeSubtree,
	}
ale's avatar
ale committed
338 339 340 341 342 343 344
}

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

ale's avatar
ale committed
345 346 347
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
348
	}
ale's avatar
ale committed
349
	rdn := replaceVars("ftpname=${resource}", templateVars{"resource": rsrc.Name})
ale's avatar
ale committed
350 351
	return joinDN(rdn, getUserDN(user, h.baseDN)), nil
}
ale's avatar
ale committed
352

ale's avatar
ale committed
353 354
func (h *webdavResourceHandler) GetOwner(rsrc *as.Resource) string {
	return ownerFromDN(rsrc.ID.String())
ale's avatar
ale committed
355 356
}

357
func (h *webdavResourceHandler) FromLDAP(entry *ldap.Entry) (*as.Resource, error) {
ale's avatar
ale committed
358 359 360 361 362
	if !isObjectClass(entry, "ftpAccount") {
		return nil, errWrongObjectClass
	}

	name := entry.GetAttributeValue("ftpname")
363 364
	uid, _ := strconv.Atoi(entry.GetAttributeValue(uidNumberLDAPAttr)) // nolint
	return &as.Resource{
ale's avatar
ale committed
365
		ID:   as.ResourceID(entry.DN),
366
		Type: as.ResourceTypeDAV,
ale's avatar
ale committed
367
		Name: name,
368
		DAV: &as.WebDAV{
ale's avatar
ale committed
369
			Homedir: entry.GetAttributeValue("homeDirectory"),
ale's avatar
ale committed
370
			UID:     uid,
ale's avatar
ale committed
371 372 373 374
		},
	}, nil
}

375
func (h *webdavResourceHandler) ToLDAP(rsrc *as.Resource) []ldap.PartialAttribute {
ale's avatar
ale committed
376 377
	return []ldap.PartialAttribute{
		{Type: "objectClass", Vals: []string{"top", "person", "posixAccount", "shadowAccount", "organizationalPerson", "inetOrgPerson", "ftpAccount"}},
ale's avatar
ale committed
378
		{Type: "ftpname", Vals: s2l(rsrc.Name)},
ale's avatar
ale committed
379
		{Type: "homeDirectory", Vals: s2l(rsrc.DAV.Homedir)},
ale's avatar
ale committed
380
		{Type: uidNumberLDAPAttr, Vals: s2l(strconv.Itoa(rsrc.DAV.UID))},
ale's avatar
ale committed
381 382 383
	}
}

384 385
func (h *webdavResourceHandler) SearchQuery() *queryTemplate {
	return &queryTemplate{
ale's avatar
ale committed
386 387
		Base:   joinDN("ou=People", h.baseDN),
		Filter: "(&(objectClass=ftpAccount)(ftpname=${resource}))",
388 389
		Scope:  ldap.ScopeWholeSubtree,
	}
ale's avatar
ale committed
390 391 392 393 394 395 396 397 398 399 400 401 402
}

// 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
403
func (h *databaseResourceHandler) MakeDN(user *as.User, rsrc *as.Resource) (string, error) {
ale's avatar
ale committed
404
	rdn := replaceVars("dbname=${resource}", templateVars{"resource": rsrc.Name})
ale's avatar
ale committed
405
	return joinDN(rdn, string(rsrc.ParentID)), nil
ale's avatar
ale committed
406 407
}

ale's avatar
ale committed
408 409
func (h *databaseResourceHandler) GetOwner(rsrc *as.Resource) string {
	return ownerFromDN(rsrc.ID.String())
ale's avatar
ale committed
410 411
}

412
func (h *databaseResourceHandler) FromLDAP(entry *ldap.Entry) (*as.Resource, error) {
ale's avatar
ale committed
413 414 415 416 417
	if !isObjectClass(entry, "dbMysql") {
		return nil, errWrongObjectClass
	}

	name := entry.GetAttributeValue("dbname")
ale's avatar
ale committed
418 419 420 421 422 423 424 425

	// 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
426
	}
427
	return &as.Resource{
ale's avatar
ale committed
428
		ID:       as.ResourceID(entry.DN),
429
		Type:     as.ResourceTypeDatabase,
ale's avatar
ale committed
430
		ParentID: as.ResourceID(parentDN),
ale's avatar
ale committed
431
		Name:     name,
432
		Database: &as.Database{
ale's avatar
ale committed
433 434 435 436 437 438
			DBUser:            entry.GetAttributeValue("dbuser"),
			CleartextPassword: entry.GetAttributeValue("clearPassword"),
		},
	}, nil
}

439
func (h *databaseResourceHandler) ToLDAP(rsrc *as.Resource) []ldap.PartialAttribute {
ale's avatar
ale committed
440 441
	return []ldap.PartialAttribute{
		{Type: "objectClass", Vals: []string{"top", "dbMysql"}},
ale's avatar
ale committed
442
		{Type: "dbname", Vals: s2l(rsrc.Name)},
ale's avatar
ale committed
443 444 445 446 447
		{Type: "dbuser", Vals: s2l(rsrc.Database.DBUser)},
		{Type: "clearPassword", Vals: s2l(rsrc.Database.CleartextPassword)},
	}
}

448 449
func (h *databaseResourceHandler) SearchQuery() *queryTemplate {
	return &queryTemplate{
ale's avatar
ale committed
450 451
		Base:   joinDN("ou=People", h.baseDN),
		Filter: "(&(objectClass=dbMysql)(dbname=${resource}))",
452 453
		Scope:  ldap.ScopeWholeSubtree,
	}
ale's avatar
ale committed
454 455
}

ale's avatar
ale committed
456 457 458 459 460 461 462 463 464 465
func newDefaultResourceRegistry(baseDN string) *resourceRegistry {
	reg := newResourceRegistry()
	reg.register(as.ResourceTypeEmail, &emailResourceHandler{baseDN: baseDN})
	reg.register(as.ResourceTypeMailingList, &mailingListResourceHandler{baseDN: baseDN})
	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
}