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 168 169 170 171 172
			Aliases: entry.GetAttributeValues("mailAlternateAddr"),
			Maildir: entry.GetAttributeValue("mailMessageStore"),
		},
	}, nil
}

173
func (h *emailResourceHandler) ToLDAP(rsrc *as.Resource) []ldap.PartialAttribute {
ale's avatar
ale committed
174 175 176 177 178 179 180 181
	return []ldap.PartialAttribute{
		{Type: "objectClass", Vals: []string{"top", "virtualMailUser"}},
		{Type: "mail", Vals: s2l(rsrc.ID.Name())},
		{Type: "mailAlternateAddr", Vals: rsrc.Email.Aliases},
		{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, ",")
}