model.go 19.6 KB
Newer Older
ale's avatar
ale committed
1 2 3 4
package backend

import (
	"context"
ale's avatar
ale committed
5 6 7
	"fmt"
	"math/rand"
	"strconv"
ale's avatar
ale committed
8
	"strings"
9
	"time"
ale's avatar
ale committed
10 11 12 13

	ldaputil "git.autistici.org/ai3/go-common/ldap"
	"gopkg.in/ldap.v2"

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

17 18
const (
	// Names of some well-known LDAP attributes.
19 20 21 22 23 24 25 26 27 28 29
	totpSecretLDAPAttr         = "totpSecret"
	preferredLanguageLDAPAttr  = "preferredLanguage"
	recoveryHintLDAPAttr       = "recoverQuestion"
	recoveryResponseLDAPAttr   = "recoverAnswer"
	aspLDAPAttr                = "appSpecificPassword"
	storagePublicKeyLDAPAttr   = "storagePublicKey"
	storagePrivateKeyLDAPAttr  = "storageEncryptedSecretKey"
	passwordLDAPAttr           = "userPassword"
	passwordLastChangeLDAPAttr = "shadowLastChange"
	u2fRegistrationsLDAPAttr   = "u2fRegistration"
	uidNumberLDAPAttr          = "uidNumber"
30 31
)

32
// backend is the interface to an LDAP-backed user database.
ale's avatar
ale committed
33 34 35 36 37
//
// We keep a set of LDAP queries for each resource type, each having a
// "resource" query to return a specific resource belonging to a user,
// and a "presence" query that checks for existence of a resource for
// all users.
38
type backend struct {
ale's avatar
ale committed
39
	conn                ldapConn
ale's avatar
ale committed
40
	baseDN              string
41
	userQuery           *queryTemplate
ale's avatar
ale committed
42
	searchUserQuery     *queryTemplate
43
	userResourceQueries []*queryTemplate
ale's avatar
ale committed
44
	resources           *resourceRegistry
ale's avatar
ale committed
45 46 47

	// Range for new user IDs.
	minUID, maxUID int
ale's avatar
ale committed
48 49
}

ale's avatar
ale committed
50 51 52 53 54
const (
	defaultMinUID = 10000
	defaultMaxUID = 1000000
)

ale's avatar
ale committed
55 56
// backendTX holds the business logic (that runs within a single
// transaction).
57 58 59 60 61
type backendTX struct {
	*ldapTX
	backend *backend
}

ale's avatar
ale committed
62 63
const ldapPoolSize = 20

64
func (b *backend) NewTransaction() (as.TX, error) {
65 66 67 68 69 70
	return &backendTX{
		ldapTX:  newLDAPTX(b.conn),
		backend: b,
	}, nil
}

ale's avatar
ale committed
71 72
// NewLDAPBackend initializes an LDAPBackend object with the given LDAP
// connection pool.
73
func NewLDAPBackend(uri, bindDN, bindPw, base string) (as.Backend, error) {
ale's avatar
ale committed
74 75 76 77
	pool, err := ldaputil.NewConnectionPool(uri, bindDN, bindPw, ldapPoolSize)
	if err != nil {
		return nil, err
	}
78 79
	return newLDAPBackendWithConn(pool, base)
}
ale's avatar
ale committed
80

ale's avatar
ale committed
81 82
func newLDAPBackendWithConn(conn ldapConn, baseDN string) (*backend, error) {
	reg := newDefaultResourceRegistry(baseDN)
ale's avatar
ale committed
83

84
	return &backend{
ale's avatar
ale committed
85
		conn:   conn,
ale's avatar
ale committed
86
		baseDN: baseDN,
87
		userQuery: &queryTemplate{
ale's avatar
ale committed
88
			Base:   joinDN("uid=${user}", "ou=People", baseDN),
89 90 91
			Filter: "(objectClass=*)",
			Scope:  ldap.ScopeBaseObject,
		},
ale's avatar
ale committed
92 93 94 95 96
		searchUserQuery: &queryTemplate{
			Base:   joinDN("ou=People", baseDN),
			Filter: "(uid=${pattern}*)",
			Scope:  ldap.ScopeSingleLevel,
		},
97
		userResourceQueries: []*queryTemplate{
ale's avatar
ale committed
98
			// Find all resources that are children of the main uid object.
99
			&queryTemplate{
ale's avatar
ale committed
100
				Base:  joinDN("uid=${user}", "ou=People", baseDN),
101 102
				Scope: ldap.ScopeWholeSubtree,
			},
ale's avatar
ale committed
103
			// Find mailing lists, which are nested under a different root.
104
			&queryTemplate{
ale's avatar
ale committed
105
				Base:   joinDN("ou=Lists", baseDN),
ale's avatar
ale committed
106
				Filter: "(&(objectClass=mailingList)(listOwner=${user}))",
107 108
				Scope:  ldap.ScopeSingleLevel,
			},
ale's avatar
ale committed
109
		},
ale's avatar
ale committed
110
		resources: reg,
ale's avatar
ale committed
111 112
		minUID:    defaultMinUID,
		maxUID:    defaultMaxUID,
ale's avatar
ale committed
113
	}, nil
ale's avatar
ale committed
114 115
}

116
func newUser(entry *ldap.Entry) (*as.RawUser, error) {
117 118 119 120 121 122 123
	// Note that some user-level attributes related to
	// authentication are stored on the uid= object, while others
	// are on the email= object. We set the latter in the GetUser
	// function later.
	//
	// The case of password recovery attributes is more complex:
	// the current schema has those on email=, but we'd like to
124 125
	// move them to uid=, so we currently have to support both
	// (but the uid= one takes precedence).
126
	uidNumber, _ := strconv.Atoi(entry.GetAttributeValue(uidNumberLDAPAttr)) // nolint
127 128
	user := &as.RawUser{
		User: as.User{
ale's avatar
ale committed
129 130 131 132 133
			Name:                    entry.GetAttributeValue("uid"),
			Lang:                    entry.GetAttributeValue(preferredLanguageLDAPAttr),
			UID:                     uidNumber,
			Status:                  entry.GetAttributeValue("status"),
			Shard:                   entry.GetAttributeValue("host"),
134 135 136 137
			LastPasswordChangeStamp: decodeShadowTimestamp(entry.GetAttributeValue(passwordLastChangeLDAPAttr)),
			AccountRecoveryHint:     entry.GetAttributeValue(recoveryHintLDAPAttr),
			U2FRegistrations:        decodeU2FRegistrations(entry.GetAttributeValues(u2fRegistrationsLDAPAttr)),
			HasOTP:                  entry.GetAttributeValue(totpSecretLDAPAttr) != "",
138 139 140 141
		},
		// Remove the legacy LDAP {crypt} prefix on old passwords.
		Password:         strings.TrimPrefix(entry.GetAttributeValue(passwordLDAPAttr), "{crypt}"),
		RecoveryPassword: strings.TrimPrefix(entry.GetAttributeValue(recoveryResponseLDAPAttr), "{crypt}"),
ale's avatar
ale committed
142
	}
143 144

	// The user has 2FA enabled if it has a TOTP secret or U2F keys.
145
	user.Has2FA = (user.HasOTP || (len(user.U2FRegistrations) > 0))
146

ale's avatar
ale committed
147 148 149 150 151 152
	if user.Lang == "" {
		user.Lang = "en"
	}
	return user, nil
}

153
func userToLDAP(user *as.User) (attrs []ldap.PartialAttribute) {
ale's avatar
ale committed
154 155
	// Most attributes are read-only and have specialized methods to set them.
	attrs = append(attrs, []ldap.PartialAttribute{
156
		{Type: "objectClass", Vals: []string{"top", "person", "posixAccount", "shadowAccount", "organizationalPerson", "inetOrgPerson", "investiciUser"}},
ale's avatar
ale committed
157 158
		{Type: "uid", Vals: s2l(user.Name)},
		{Type: "cn", Vals: s2l(user.Name)},
ale's avatar
ale committed
159
		{Type: uidNumberLDAPAttr, Vals: s2l(strconv.Itoa(user.UID))},
160 161
		{Type: "givenName", Vals: s2l("Private")},
		{Type: "sn", Vals: s2l("Private")},
ale's avatar
ale committed
162
		{Type: "gecos", Vals: s2l(user.Name)},
163 164 165 166 167 168 169
		{Type: "loginShell", Vals: s2l("/bin/false")},
		{Type: "homeDirectory", Vals: s2l("/var/empty")},
		{Type: passwordLastChangeLDAPAttr, Vals: s2l("12345")},
		{Type: "status", Vals: s2l(user.Status)},
		{Type: "host", Vals: s2l(user.Shard)},
		{Type: "shadowWarning", Vals: s2l("7")},
		{Type: "shadowMax", Vals: s2l("99999")},
ale's avatar
ale committed
170
		{Type: preferredLanguageLDAPAttr, Vals: s2l(user.Lang)},
171
		{Type: u2fRegistrationsLDAPAttr, Vals: encodeU2FRegistrations(user.U2FRegistrations)},
ale's avatar
ale committed
172 173 174 175
	}...)
	return
}

176
func (tx *backendTX) getUserDN(user *as.User) string {
ale's avatar
ale committed
177 178 179 180 181
	return getUserDN(user, tx.backend.baseDN)
}

func getUserDN(user *as.User, baseDN string) string {
	return joinDN("uid="+user.Name, "ou=People", baseDN)
ale's avatar
ale committed
182 183
}

ale's avatar
ale committed
184
// CreateUser creates a new user.
ale's avatar
ale committed
185
func (tx *backendTX) CreateUser(ctx context.Context, user *as.User) (*as.User, error) {
ale's avatar
ale committed
186 187 188 189 190 191 192 193
	dn := tx.getUserDN(user)

	tx.create(dn)
	for _, attr := range userToLDAP(user) {
		tx.setAttr(dn, attr.Type, attr.Vals...)
	}

	// Create all resources.
ale's avatar
ale committed
194 195 196
	rsrcs, err := tx.CreateResources(ctx, user, user.Resources)
	if err != nil {
		return nil, err
ale's avatar
ale committed
197
	}
ale's avatar
ale committed
198
	user.Resources = rsrcs
ale's avatar
ale committed
199

ale's avatar
ale committed
200
	return user, nil
ale's avatar
ale committed
201 202
}

203
// UpdateUser updates values for the user only (not the resources).
204
func (tx *backendTX) UpdateUser(ctx context.Context, user *as.User) error {
205 206 207 208 209 210 211
	dn := tx.getUserDN(user)
	for _, attr := range userToLDAP(user) {
		tx.setAttr(dn, attr.Type, attr.Vals...)
	}
	return nil
}

ale's avatar
ale committed
212
// GetUser returns a user.
213
func (tx *backendTX) GetUser(ctx context.Context, username string) (*as.RawUser, error) {
ale's avatar
ale committed
214
	// First of all, find the main user object, and just that one.
ale's avatar
ale committed
215
	vars := templateVars{"user": username}
216
	result, err := tx.search(ctx, tx.backend.userQuery.query(vars))
ale's avatar
ale committed
217 218 219 220 221 222
	if err != nil {
		if ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) {
			return nil, nil
		}
		return nil, err
	}
223 224 225
	if len(result.Entries) == 0 {
		return nil, nil
	}
ale's avatar
ale committed
226 227 228 229 230 231 232 233 234 235

	user, err := newUser(result.Entries[0])
	if err != nil {
		return nil, err
	}

	// Now run the resource queries, and accumulate results on the User
	// object we just created.
	// TODO: parallelize.
	// TODO: add support for non-LDAP resource queries.
236 237
	for _, tpl := range tx.backend.userResourceQueries {
		result, err = tx.search(ctx, tpl.query(vars))
ale's avatar
ale committed
238 239 240 241 242
		if err != nil {
			continue
		}

		for _, entry := range result.Entries {
243 244 245 246 247 248
			// Some user-level attributes are actually stored on
			// the email object, which is desired in some cases,
			// but in others is a shortcoming of the legacy A/I
			// database model. Set them on the main User
			// object. For the latter, attributes on the main User
			// object take precedence.
ale's avatar
ale committed
249
			if isObjectClass(entry, "virtualMailUser") {
250 251 252 253
				if user.AccountRecoveryHint == "" {
					if s := entry.GetAttributeValue(recoveryHintLDAPAttr); s != "" {
						user.AccountRecoveryHint = s
					}
254
				}
255

256
				user.AppSpecificPasswords = getASPInfo(decodeAppSpecificPasswords(entry.GetAttributeValues(aspLDAPAttr)))
257 258
				user.Keys = decodeUserEncryptionKeys(
					entry.GetAttributeValues(storagePrivateKeyLDAPAttr))
259
				user.HasEncryptionKeys = (entry.GetAttributeValue(storagePublicKeyLDAPAttr) != "")
ale's avatar
ale committed
260
			}
ale's avatar
ale committed
261

ale's avatar
ale committed
262
			// Parse the resource and add it to the User.
ale's avatar
ale committed
263
			if r, err := tx.backend.resources.FromLDAP(entry); err == nil {
ale's avatar
ale committed
264 265 266 267 268 269 270 271
				user.Resources = append(user.Resources, r)
			}
		}
	}

	return user, nil
}

ale's avatar
ale committed
272 273
func (tx *backendTX) SearchUser(ctx context.Context, pattern string) ([]string, error) {
	// First of all, find the main user object, and just that one.
ale's avatar
ale committed
274
	vars := templateVars{"pattern": rawVariable(pattern)}
ale's avatar
ale committed
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289
	result, err := tx.search(ctx, tx.backend.searchUserQuery.query(vars))
	if err != nil {
		return nil, err
	}
	if len(result.Entries) == 0 {
		return nil, nil
	}

	var out []string
	for _, entry := range result.Entries {
		out = append(out, entry.GetAttributeValue("uid"))
	}
	return out, nil
}

290
func (tx *backendTX) SetUserPassword(ctx context.Context, user *as.User, encryptedPassword string) (err error) {
291 292
	dn := tx.getUserDN(user)
	tx.setAttr(dn, passwordLDAPAttr, encryptedPassword)
293
	tx.setAttr(dn, passwordLastChangeLDAPAttr, encodeShadowTimestamp(time.Now()))
294 295 296 297 298
	for _, r := range user.GetResourcesByType(as.ResourceTypeEmail) {
		dn, err = tx.backend.resources.GetDN(r.ID)
		if err != nil {
			return
		}
299 300
		tx.setAttr(dn, passwordLDAPAttr, encryptedPassword)
	}
301
	return
ale's avatar
ale committed
302 303
}

304
func (tx *backendTX) SetAccountRecoveryHint(ctx context.Context, user *as.User, hint, response string) error {
305 306
	// Write the password recovery attributes on the uid= object,
	// as per the new schema.
ale's avatar
ale committed
307 308 309 310 311 312
	dn := tx.getUserDN(user)
	tx.setAttr(dn, recoveryHintLDAPAttr, hint)
	tx.setAttr(dn, recoveryResponseLDAPAttr, response)
	return nil
}

313
func (tx *backendTX) DeleteAccountRecoveryHint(ctx context.Context, user *as.User) error {
314 315 316 317 318 319
	// Write the password recovery attributes on the uid= object,
	// as per the new schema.
	dn := tx.getUserDN(user)
	tx.setAttr(dn, recoveryHintLDAPAttr)
	tx.setAttr(dn, recoveryResponseLDAPAttr)
	return nil
ale's avatar
ale committed
320 321
}

322
func (tx *backendTX) SetUserEncryptionKeys(ctx context.Context, user *as.User, keys []*as.UserEncryptionKey) error {
323
	encKeys := encodeUserEncryptionKeys(keys)
324 325 326 327 328
	for _, r := range user.GetResourcesByType(as.ResourceTypeEmail) {
		dn, err := tx.backend.resources.GetDN(r.ID)
		if err != nil {
			return err
		}
329 330
		tx.setAttr(dn, storagePrivateKeyLDAPAttr, encKeys...)
	}
331
	return nil
ale's avatar
ale committed
332 333
}

334 335 336 337 338 339
func (tx *backendTX) SetUserEncryptionPublicKey(ctx context.Context, user *as.User, pub []byte) error {
	for _, r := range user.GetResourcesByType(as.ResourceTypeEmail) {
		dn, err := tx.backend.resources.GetDN(r.ID)
		if err != nil {
			return err
		}
340 341
		tx.setAttr(dn, storagePublicKeyLDAPAttr, string(pub))
	}
342 343 344 345 346 347 348 349 350
	return nil
}

func excludeASPFromList(asps []*appSpecificPassword, id string) []*appSpecificPassword {
	var out []*appSpecificPassword
	for _, asp := range asps {
		if asp.ID != id {
			out = append(out, asp)
		}
351
	}
352
	return out
353 354
}

ale's avatar
ale committed
355 356
func (tx *backendTX) SetApplicationSpecificPassword(ctx context.Context, user *as.User, info *as.AppSpecificPasswordInfo, encryptedPassword string) error {
	dn := tx.getUserDN(user)
357
	asps := decodeAppSpecificPasswords(tx.readAttributeValues(ctx, dn, aspLDAPAttr))
358 359
	asps = append(excludeASPFromList(asps, info.ID), newAppSpecificPassword(*info, encryptedPassword))
	outASPs := encodeAppSpecificPasswords(asps)
360 361 362
	tx.setAttr(dn, aspLDAPAttr, outASPs...)
	return nil
}
ale's avatar
ale committed
363

ale's avatar
ale committed
364 365
func (tx *backendTX) DeleteApplicationSpecificPassword(ctx context.Context, user *as.User, id string) error {
	dn := tx.getUserDN(user)
366
	asps := decodeAppSpecificPasswords(tx.readAttributeValues(ctx, dn, aspLDAPAttr))
367 368
	asps = excludeASPFromList(asps, id)
	outASPs := encodeAppSpecificPasswords(asps)
369
	tx.setAttr(dn, aspLDAPAttr, outASPs...)
370
	return nil
ale's avatar
ale committed
371 372
}

373
func (tx *backendTX) SetUserTOTPSecret(ctx context.Context, user *as.User, secret string) error {
374
	tx.setAttr(tx.getUserDN(user), totpSecretLDAPAttr, secret)
375
	return nil
ale's avatar
ale committed
376 377
}

378
func (tx *backendTX) DeleteUserTOTPSecret(ctx context.Context, user *as.User) error {
379
	tx.setAttr(tx.getUserDN(user), totpSecretLDAPAttr)
380
	return nil
ale's avatar
ale committed
381 382
}

383 384 385 386 387
func (tx *backendTX) SetResourcePassword(ctx context.Context, r *as.Resource, encryptedPassword string) error {
	dn, err := tx.backend.resources.GetDN(r.ID)
	if err != nil {
		return err
	}
388
	tx.setAttr(dn, passwordLDAPAttr, encryptedPassword)
389
	return nil
ale's avatar
ale committed
390 391
}

ale's avatar
ale committed
392
func (tx *backendTX) hasResource(ctx context.Context, resourceType, resourceName string) (bool, error) {
393
	tpl, err := tx.backend.resources.SearchQuery(resourceType)
ale's avatar
ale committed
394 395
	if err != nil {
		return false, err
ale's avatar
ale committed
396 397 398
	}

	// Make a quick LDAP search that only fetches the DN attribute.
399
	tpl.Attrs = []string{"dn"}
ale's avatar
ale committed
400
	result, err := tx.search(ctx, tpl.query(templateVars{
ale's avatar
ale committed
401 402
		"resource": resourceName,
		"type":     resourceType,
403
	}))
ale's avatar
ale committed
404 405 406 407 408 409 410 411 412 413 414 415 416
	if err != nil {
		if ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) {
			return false, nil
		}
		return false, err
	}
	if len(result.Entries) == 0 {
		return false, nil
	}
	return true, nil
}

// HasAnyResource returns true if any of the specified resources exists.
417
func (tx *backendTX) HasAnyResource(ctx context.Context, resourceIDs []as.FindResourceRequest) (bool, error) {
ale's avatar
ale committed
418 419
	for _, req := range resourceIDs {
		has, err := tx.hasResource(ctx, req.Type, req.Name)
ale's avatar
ale committed
420 421 422 423 424 425 426
		if err != nil || has {
			return has, err
		}
	}
	return false, nil
}

ale's avatar
ale committed
427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473
func (tx *backendTX) searchResourcesByType(ctx context.Context, pattern, resourceType string) ([]*as.RawResource, error) {
	tpl, err := tx.backend.resources.SearchQuery(resourceType)
	if err != nil {
		return nil, err
	}
	result, err := tx.search(ctx, tpl.query(templateVars{
		"resource": rawVariable(pattern),
		"type":     resourceType,
	}))
	if err != nil {
		if ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) {
			return nil, nil
		}
		return nil, err
	}
	if len(result.Entries) == 0 {
		return nil, nil
	}

	var out []*as.RawResource
	for _, entry := range result.Entries {
		rsrc, err := tx.backend.resources.FromLDAP(entry)
		if err != nil {
			return nil, err
		}
		out = append(out, &as.RawResource{
			Resource: *rsrc,
			Owner:    tx.backend.resources.GetOwner(rsrc),
		})
	}
	return out, nil
}

// SearchResource returns all the resources matching the pattern.
func (tx *backendTX) SearchResource(ctx context.Context, pattern string) ([]*as.RawResource, error) {
	// Aggregate results for all known resource types.
	var out []*as.RawResource
	for _, typ := range tx.backend.resources.types {
		r, err := tx.searchResourcesByType(ctx, pattern, typ)
		if err != nil {
			return nil, err
		}
		out = append(out, r...)
	}
	return out, nil
}

474
// GetResource returns a ResourceWrapper, as part of a read-modify-update transaction.
ale's avatar
ale committed
475
func (tx *backendTX) GetResource(ctx context.Context, rsrcID as.ResourceID) (*as.RawResource, error) {
ale's avatar
ale committed
476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496
	// From the resource ID we can obtain the DN, and fetch it
	// straight from LDAP without even doing a real search.
	dn, err := tx.backend.resources.GetDN(rsrcID)
	if err != nil {
		return nil, err
	}

	// This is just a 'point' search.
	req := ldap.NewSearchRequest(
		dn,
		ldap.ScopeBaseObject,
		ldap.NeverDerefAliases,
		0,
		0,
		false,
		"(objectClass=*)",
		nil,
		nil,
	)

	result, err := tx.search(ctx, req)
497 498 499 500 501 502 503
	if err != nil {
		if ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) {
			return nil, nil
		}
		return nil, err
	}

ale's avatar
ale committed
504 505 506 507 508 509 510 511 512
	rsrc, err := tx.backend.resources.FromLDAP(result.Entries[0])
	if err != nil {
		return nil, err
	}

	return &as.RawResource{
		Resource: *rsrc,
		Owner:    tx.backend.resources.GetOwner(rsrc),
	}, nil
513 514
}

ale's avatar
ale committed
515 516 517
// Create a single resource, modify its ID in-place. Ignores previous ID value.
func (tx *backendTX) createSingleResource(user *as.User, rsrc *as.Resource) error {
	dn, err := tx.backend.resources.MakeDN(user, rsrc)
ale's avatar
ale committed
518 519 520 521
	if err != nil {
		return err
	}

ale's avatar
ale committed
522 523 524 525
	// Here we assign resource IDs.
	rsrc.ID = as.ResourceID(dn)

	// Now write the resource data to LDAP.
ale's avatar
ale committed
526
	tx.create(dn)
ale's avatar
ale committed
527
	for _, attr := range tx.backend.resources.ToLDAP(rsrc) {
ale's avatar
ale committed
528 529 530 531 532 533
		tx.setAttr(dn, attr.Type, attr.Vals...)
	}

	return nil
}

ale's avatar
ale committed
534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584
// Sort the resource tree (defined by parent_id relations) breadth-first.
// Horrible algorithm: inefficient on large lists, never terminates on loops.
func sortResourcesByDepth(rsrcs []*as.Resource) []*as.Resource {
	var out []*as.Resource
	stack := []as.ResourceID{as.ResourceID("")}
	for len(stack) > 0 {
		cur := stack[0]
		stack = stack[1:]
		var left []*as.Resource
		for _, r := range rsrcs {
			if r.ParentID.Equal(cur) {
				out = append(out, r)
				stack = append(stack, r.ID)
			} else {
				left = append(left, r)
			}
		}
		rsrcs = left
	}
	return out
}

// CreateResources creates new LDAP-backed resource objects. It
// modifies the input resource objects in-place and assigns them a
// ResourceID. Input resource IDs are ignored (they can be empty),
// with one exception: in order to resolve ParentIDs properly,
// resource IDs can be set to user-selected values and then referenced
// in another resource's ParentID. In this case, the resulting
// ParentID will be then set to the real resource ID of the parent
// resource.
func (tx *backendTX) CreateResources(ctx context.Context, user *as.User, rsrcs []*as.Resource) ([]*as.Resource, error) {
	idMap := make(map[as.ResourceID]as.ResourceID)
	for _, rsrc := range sortResourcesByDepth(rsrcs) {
		oldID := rsrc.ID
		if !rsrc.ParentID.Empty() {
			pid, ok := idMap[rsrc.ParentID]
			if !ok {
				return nil, fmt.Errorf("resource %s references unknown parent_id %s", rsrc.String(), rsrc.ParentID.String())
			}
			rsrc.ParentID = pid
		}
		if err := tx.createSingleResource(user, rsrc); err != nil {
			return nil, err
		}
		if !oldID.Empty() {
			idMap[oldID] = rsrc.ID
		}
	}
	return rsrcs, nil
}

585
// UpdateResource updates a LDAP-backed resource that was obtained by a previous GetResource call.
586
func (tx *backendTX) UpdateResource(ctx context.Context, r *as.Resource) error {
ale's avatar
ale committed
587 588 589 590
	dn, err := tx.backend.resources.GetDN(r.ID)
	if err != nil {
		return err
	}
591

592 593
	// We can simply dump all attribute/value pairs and let the
	// code in ldapTX do the work of finding the differences.
ale's avatar
ale committed
594
	for _, attr := range tx.backend.resources.ToLDAP(r) {
595
		tx.setAttr(dn, attr.Type, attr.Vals...)
596 597
	}

598
	return nil
599 600
}

ale's avatar
ale committed
601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640
// NextUID returns an available user ID (uidNumber). It does so
// without keeping state by just guessing random UIDs in the
// minUID-maxUID range, and checking if they are available (so it's
// best to keep the range largely underutilized).
func (tx *backendTX) NextUID(ctx context.Context) (uid int, err error) {
	// Won't actually run forever, at some point the context will expire.
	var ok bool
	for !ok && err == nil {
		uid = tx.backend.minUID + rand.Intn(tx.backend.maxUID-tx.backend.minUID)
		ok, err = tx.isUIDAvailable(ctx, uid)
	}
	return
}

func (tx *backendTX) isUIDAvailable(ctx context.Context, uid int) (bool, error) {
	// Try to make this query lightweight: ask for no attributes,
	// use a size limit of 1 and treat "Size Limit Exceeded"
	// errors as a successful result.
	result, err := tx.search(ctx, ldap.NewSearchRequest(
		tx.backend.baseDN,
		ldap.ScopeWholeSubtree,
		ldap.NeverDerefAliases,
		1, // just one result is enough
		0,
		false,
		fmt.Sprintf("(uidNumber=%d)", uid),
		[]string{"dn"},
		nil,
	))
	if err != nil {
		if ldap.IsErrorWithCode(err, ldap.LDAPResultSizeLimitExceeded) {
			return false, nil
		}
		return false, err
	}
	if len(result.Entries) > 0 {
		return false, nil
	}
	return true, nil
}
641

ale's avatar
ale committed
642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659
func (tx *backendTX) CanAccessResource(_ context.Context, username string, rsrc *as.Resource) bool {
	switch rsrc.Type {
	case as.ResourceTypeMailingList:
		for _, a := range rsrc.List.Admins {
			if a == username {
				return true
			}
		}
		return false
	default:
		dn, err := tx.backend.resources.GetDN(rsrc.ID)
		if err != nil {
			return false
		}
		owner := ownerFromDN(dn)
		return owner == username
	}
}