model.go 14.3 KB
Newer Older
ale's avatar
ale committed
1
2
3
4
5
6
7
8
9
10
11
12
package backend

import (
	"context"
	"strings"

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

	"git.autistici.org/ai3/accountserver"
)

13
14
15
16
const (
	// Names of some well-known LDAP attributes.
	totpSecretLDAPAttr        = "totpSecret"
	preferredLanguageLDAPAttr = "preferredLanguage"
ale's avatar
ale committed
17
18
	recoveryHintLDAPAttr      = "recoverQuestion"
	recoveryResponseLDAPAttr  = "recoverAnswer"
19
20
21
22
23
24
	aspLDAPAttr               = "appSpecificPassword"
	storagePublicKeyLDAPAttr  = "storagePublicKey"
	storagePrivateKeyLDAPAttr = "storageEncryptedSecretKey"
	passwordLDAPAttr          = "userPassword"
)

25
// backend is the interface to an LDAP-backed user database.
ale's avatar
ale committed
26
27
28
29
30
//
// 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.
31
type backend struct {
ale's avatar
ale committed
32
	conn                ldapConn
ale's avatar
ale committed
33
	baseDN              string
34
35
	userQuery           *queryTemplate
	userResourceQueries []*queryTemplate
ale's avatar
ale committed
36
	resources           *resourceRegistry
ale's avatar
ale committed
37
38
}

ale's avatar
ale committed
39
40
// backendTX holds the business logic (that runs within a single
// transaction).
41
42
43
44
45
type backendTX struct {
	*ldapTX
	backend *backend
}

ale's avatar
ale committed
46
47
const ldapPoolSize = 20

48
49
50
51
52
53
54
func (b *backend) NewTransaction() (accountserver.TX, error) {
	return &backendTX{
		ldapTX:  newLDAPTX(b.conn),
		backend: b,
	}, nil
}

ale's avatar
ale committed
55
56
// NewLDAPBackend initializes an LDAPBackend object with the given LDAP
// connection pool.
57
func NewLDAPBackend(uri, bindDN, bindPw, base string) (accountserver.Backend, error) {
ale's avatar
ale committed
58
59
60
61
	pool, err := ldaputil.NewConnectionPool(uri, bindDN, bindPw, ldapPoolSize)
	if err != nil {
		return nil, err
	}
62
63
	return newLDAPBackendWithConn(pool, base)
}
ale's avatar
ale committed
64

65
func newLDAPBackendWithConn(conn ldapConn, base string) (*backend, error) {
ale's avatar
ale committed
66
67
68
69
70
71
72
73
	rsrc := newResourceRegistry()
	rsrc.register(accountserver.ResourceTypeEmail, &emailResourceHandler{baseDN: base})
	rsrc.register(accountserver.ResourceTypeMailingList, &mailingListResourceHandler{baseDN: base})
	rsrc.register(accountserver.ResourceTypeDAV, &webdavResourceHandler{baseDN: base})
	rsrc.register(accountserver.ResourceTypeWebsite, &websiteResourceHandler{baseDN: base})
	rsrc.register(accountserver.ResourceTypeDomain, &domainResourceHandler{baseDN: base})
	rsrc.register(accountserver.ResourceTypeDatabase, &databaseResourceHandler{baseDN: base})

74
	return &backend{
ale's avatar
ale committed
75
76
		conn:   conn,
		baseDN: base,
77
78
79
80
81
82
		userQuery: &queryTemplate{
			Base:   joinDN("uid=${user}", "ou=People", base),
			Filter: "(objectClass=*)",
			Scope:  ldap.ScopeBaseObject,
		},
		userResourceQueries: []*queryTemplate{
ale's avatar
ale committed
83
			// Find all resources that are children of the main uid object.
84
85
86
87
			&queryTemplate{
				Base:  joinDN("uid=${user}", "ou=People", base),
				Scope: ldap.ScopeWholeSubtree,
			},
ale's avatar
ale committed
88
			// Find mailing lists, which are nested under a different root.
89
90
			&queryTemplate{
				Base:   joinDN("ou=Lists", base),
ale's avatar
ale committed
91
				Filter: "(&(objectClass=mailingList)(listOwner=${user}))",
92
93
				Scope:  ldap.ScopeSingleLevel,
			},
ale's avatar
ale committed
94
		},
ale's avatar
ale committed
95
		resources: rsrc,
ale's avatar
ale committed
96
	}, nil
ale's avatar
ale committed
97
98
99
100
}

func newUser(entry *ldap.Entry) (*accountserver.User, error) {
	user := &accountserver.User{
101
102
103
104
		Name:   entry.GetAttributeValue("uid"),
		Lang:   entry.GetAttributeValue(preferredLanguageLDAPAttr),
		Has2FA: (entry.GetAttributeValue(totpSecretLDAPAttr) != ""),
		//HasEncryptionKeys: (len(entry.GetAttributeValues("storageEncryptionKey")) > 0),
ale's avatar
ale committed
105
106
107
108
109
110
111
112
		//PasswordRecoveryHint: entry.GetAttributeValue("recoverQuestion"),
	}
	if user.Lang == "" {
		user.Lang = "en"
	}
	return user, nil
}

ale's avatar
ale committed
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
func userToLDAP(user *accountserver.User) (attrs []ldap.PartialAttribute) {
	// Most attributes are read-only and have specialized methods to set them.
	attrs = append(attrs, []ldap.PartialAttribute{
		{Type: "objectClass", Vals: []string{"top", "person", "posixAccount", "shadowAccount", "organizationalPerson", "inetOrgPerson", "totpAccount"}},
		{Type: "uid", Vals: s2l(user.Name)},
		{Type: "cn", Vals: s2l(user.Name)},
		{Type: "givenName", Vals: []string{"Private"}},
		{Type: "sn", Vals: []string{"Private"}},
		{Type: "gecos", Vals: s2l(user.Name)},
		{Type: "loginShell", Vals: []string{"/bin/false"}},
		{Type: "homeDirectory", Vals: []string{"/var/empty"}},
		{Type: "shadowLastChange", Vals: []string{"12345"}},
		{Type: "shadowWarning", Vals: []string{"7"}},
		{Type: "shadowMax", Vals: []string{"99999"}},
		{Type: preferredLanguageLDAPAttr, Vals: s2l(user.Lang)},
	}...)
	return
}

ale's avatar
ale committed
132
133
func (tx *backendTX) getUserDN(user *accountserver.User) string {
	return joinDN("uid="+user.Name, "ou=People", tx.backend.baseDN)
ale's avatar
ale committed
134
135
}

ale's avatar
ale committed
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
// CreateUser creates a new user.
func (tx *backendTX) CreateUser(ctx context.Context, user *accountserver.User) error {
	dn := tx.getUserDN(user)

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

	// Create all resources.
	for _, r := range user.Resources {
		if err := tx.CreateResource(ctx, r); err != nil {
			return err
		}
	}

	return nil
}

ale's avatar
ale committed
155
// GetUser returns a user.
156
func (tx *backendTX) GetUser(ctx context.Context, username string) (*accountserver.User, error) {
ale's avatar
ale committed
157
158
	// First of all, find the main user object, and just that one.
	vars := map[string]string{"user": username}
159
	result, err := tx.search(ctx, tx.backend.userQuery.query(vars))
ale's avatar
ale committed
160
161
162
163
164
165
	if err != nil {
		if ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) {
			return nil, nil
		}
		return nil, err
	}
166
167
168
	if len(result.Entries) == 0 {
		return nil, nil
	}
ale's avatar
ale committed
169
170
171
172
173
174
175
176
177
178

	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.
179
180
	for _, tpl := range tx.backend.userResourceQueries {
		result, err = tx.search(ctx, tpl.query(vars))
ale's avatar
ale committed
181
182
183
184
185
186
187
188
189
		if err != nil {
			continue
		}

		for _, entry := range result.Entries {
			// Some user-level attributes are actually stored on the email
			// object, a shortcoming of the legacy A/I database model. Set
			// them on the main User object.
			if isObjectClass(entry, "virtualMailUser") {
ale's avatar
ale committed
190
				user.PasswordRecoveryHint = entry.GetAttributeValue(recoveryHintLDAPAttr)
191
192
				user.AppSpecificPasswords = getASPInfo(decodeAppSpecificPasswords(entry.GetAttributeValues(aspLDAPAttr)))
				user.HasEncryptionKeys = (entry.GetAttributeValue(storagePublicKeyLDAPAttr) != "")
ale's avatar
ale committed
193
			}
ale's avatar
ale committed
194

ale's avatar
ale committed
195
			// Parse the resource and add it to the User.
ale's avatar
ale committed
196
			if r, err := tx.backend.resources.FromLDAP(entry); err == nil {
ale's avatar
ale committed
197
198
199
200
201
202
203
204
205
206
				user.Resources = append(user.Resources, r)
			}
		}
	}

	groupWebResourcesByHomedir(user.Resources)

	return user, nil
}

207
func (tx *backendTX) SetUserPassword(ctx context.Context, user *accountserver.User, encryptedPassword string) error {
208
209
210
211
212
213
	dn := tx.getUserDN(user)
	tx.setAttr(dn, passwordLDAPAttr, encryptedPassword)
	for _, r := range user.GetResourcesByType(accountserver.ResourceTypeEmail) {
		dn, _ = tx.backend.resources.GetDN(r.ID)
		tx.setAttr(dn, passwordLDAPAttr, encryptedPassword)
	}
214
	return nil
ale's avatar
ale committed
215
216
}

ale's avatar
ale committed
217
218
219
220
221
222
223
func (tx *backendTX) SetPasswordRecoveryHint(ctx context.Context, user *accountserver.User, hint, response string) error {
	dn := tx.getUserDN(user)
	tx.setAttr(dn, recoveryHintLDAPAttr, hint)
	tx.setAttr(dn, recoveryResponseLDAPAttr, response)
	return nil
}

224
func (tx *backendTX) GetUserEncryptionKeys(ctx context.Context, user *accountserver.User) ([]*accountserver.UserEncryptionKey, error) {
225
226
227
	r := user.GetSingleResourceByType(accountserver.ResourceTypeEmail)
	dn, _ := tx.backend.resources.GetDN(r.ID)
	rawKeys := tx.readAttributeValues(ctx, dn, storagePrivateKeyLDAPAttr)
228
	return decodeUserEncryptionKeys(rawKeys), nil
ale's avatar
ale committed
229
230
}

231
func (tx *backendTX) SetUserEncryptionKeys(ctx context.Context, user *accountserver.User, keys []*accountserver.UserEncryptionKey) error {
232
	encKeys := encodeUserEncryptionKeys(keys)
233
234
235
236
	for _, r := range user.GetResourcesByType(accountserver.ResourceTypeEmail) {
		dn, _ := tx.backend.resources.GetDN(r.ID)
		tx.setAttr(dn, storagePrivateKeyLDAPAttr, encKeys...)
	}
237
	return nil
ale's avatar
ale committed
238
239
}

240
func (tx *backendTX) SetUserEncryptionPublicKey(ctx context.Context, user *accountserver.User, pub []byte) error {
241
242
243
244
	for _, r := range user.GetResourcesByType(accountserver.ResourceTypeEmail) {
		dn, _ := tx.backend.resources.GetDN(r.ID)
		tx.setAttr(dn, storagePublicKeyLDAPAttr, string(pub))
	}
245
246
247
248
249
250
251
252
253
	return nil
}

func excludeASPFromList(asps []*appSpecificPassword, id string) []*appSpecificPassword {
	var out []*appSpecificPassword
	for _, asp := range asps {
		if asp.ID != id {
			out = append(out, asp)
		}
254
	}
255
	return out
256
257
}

258
259
func (tx *backendTX) setASPOnResource(ctx context.Context, r *accountserver.Resource, info *accountserver.AppSpecificPasswordInfo, encryptedPassword string) {
	dn, _ := tx.backend.resources.GetDN(r.ID)
ale's avatar
ale committed
260

261
	// Obtain the full list of ASPs from the backend and replace/append the new one.
262
	asps := decodeAppSpecificPasswords(tx.readAttributeValues(ctx, dn, aspLDAPAttr))
263
264
	asps = append(excludeASPFromList(asps, info.ID), newAppSpecificPassword(*info, encryptedPassword))
	outASPs := encodeAppSpecificPasswords(asps)
265
	tx.setAttr(dn, aspLDAPAttr, outASPs...)
ale's avatar
ale committed
266
267
}

268
269
270
func (tx *backendTX) SetApplicationSpecificPassword(ctx context.Context, user *accountserver.User, info *accountserver.AppSpecificPasswordInfo, encryptedPassword string) error {
	for _, r := range user.GetResourcesByType(accountserver.ResourceTypeEmail) {
		tx.setASPOnResource(ctx, r, info, encryptedPassword)
ale's avatar
ale committed
271
	}
272
273
	return nil
}
ale's avatar
ale committed
274

275
276
277
func (tx *backendTX) deleteASPOnResource(ctx context.Context, r *accountserver.Resource, id string) {
	dn, _ := tx.backend.resources.GetDN(r.ID)
	asps := decodeAppSpecificPasswords(tx.readAttributeValues(ctx, dn, aspLDAPAttr))
278
279
	asps = excludeASPFromList(asps, id)
	outASPs := encodeAppSpecificPasswords(asps)
280
281
	tx.setAttr(dn, aspLDAPAttr, outASPs...)
}
ale's avatar
ale committed
282

283
284
285
286
func (tx *backendTX) DeleteApplicationSpecificPassword(ctx context.Context, user *accountserver.User, id string) error {
	for _, r := range user.GetResourcesByType(accountserver.ResourceTypeEmail) {
		tx.deleteASPOnResource(ctx, r, id)
	}
287
	return nil
ale's avatar
ale committed
288
289
}

290
func (tx *backendTX) SetUserTOTPSecret(ctx context.Context, user *accountserver.User, secret string) error {
291
	tx.setAttr(tx.getUserDN(user), totpSecretLDAPAttr, secret)
292
	return nil
ale's avatar
ale committed
293
294
}

295
func (tx *backendTX) DeleteUserTOTPSecret(ctx context.Context, user *accountserver.User) error {
296
	tx.setAttr(tx.getUserDN(user), totpSecretLDAPAttr)
297
	return nil
ale's avatar
ale committed
298
299
}

300
func (tx *backendTX) SetResourcePassword(ctx context.Context, r *accountserver.Resource, encryptedPassword string) error {
ale's avatar
ale committed
301
	dn, _ := tx.backend.resources.GetDN(r.ID)
302
	tx.setAttr(dn, passwordLDAPAttr, encryptedPassword)
303
	return nil
ale's avatar
ale committed
304
305
}

ale's avatar
ale committed
306
func (tx *backendTX) hasResource(ctx context.Context, resourceType, resourceName string) (bool, error) {
307
	tpl, err := tx.backend.resources.SearchQuery(resourceType)
ale's avatar
ale committed
308
309
	if err != nil {
		return false, err
ale's avatar
ale committed
310
311
312
	}

	// Make a quick LDAP search that only fetches the DN attribute.
313
314
	tpl.Attrs = []string{"dn"}
	result, err := tx.search(ctx, tpl.query(map[string]string{
ale's avatar
ale committed
315
316
		"resource": resourceName,
		"type":     resourceType,
317
	}))
ale's avatar
ale committed
318
319
320
321
322
323
324
325
326
327
328
329
330
	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.
ale's avatar
ale committed
331
332
333
func (tx *backendTX) HasAnyResource(ctx context.Context, resourceIDs []accountserver.FindResourceRequest) (bool, error) {
	for _, req := range resourceIDs {
		has, err := tx.hasResource(ctx, req.Type, req.Name)
ale's avatar
ale committed
334
335
336
337
338
339
340
		if err != nil || has {
			return has, err
		}
	}
	return false, nil
}

341
// GetResource returns a ResourceWrapper, as part of a read-modify-update transaction.
342
func (tx *backendTX) GetResource(ctx context.Context, rsrcID accountserver.ResourceID) (*accountserver.Resource, error) {
ale's avatar
ale committed
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
	// 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)
364
365
366
367
368
369
370
	if err != nil {
		if ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) {
			return nil, nil
		}
		return nil, err
	}

ale's avatar
ale committed
371
372
	// We know the resource type so we don't have to guess.
	return tx.backend.resources.FromLDAPWithType(rsrcID.Type(), result.Entries[0])
373
374
}

ale's avatar
ale committed
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
// CreateResource creates a new LDAP-backed resource object.
func (tx *backendTX) CreateResource(ctx context.Context, r *accountserver.Resource) error {
	dn, err := tx.backend.resources.GetDN(r.ID)
	if err != nil {
		return err
	}

	tx.create(dn)
	for _, attr := range tx.backend.resources.ToLDAP(r) {
		tx.setAttr(dn, attr.Type, attr.Vals...)
	}

	return nil
}

390
// UpdateResource updates a LDAP-backed resource that was obtained by a previous GetResource call.
391
func (tx *backendTX) UpdateResource(ctx context.Context, r *accountserver.Resource) error {
ale's avatar
ale committed
392
393
394
395
	dn, err := tx.backend.resources.GetDN(r.ID)
	if err != nil {
		return err
	}
396

397
398
	// 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
399
	for _, attr := range tx.backend.resources.ToLDAP(r) {
400
		tx.setAttr(dn, attr.Type, attr.Vals...)
401
402
	}

403
	return nil
404
405
}

ale's avatar
ale committed
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
var siteRoot = "/home/users/investici.org/"

// The hosting directory for a website is the path component immediately after
// siteRoot. This works also for sites with nested documentRoots.
func getHostingDir(path string) string {
	path = strings.TrimPrefix(path, siteRoot)
	if i := strings.Index(path, "/"); i > 0 {
		return path[:i]
	}
	return path
}

// This is a very specific function meant to address a peculiar characteristic
// of the A/I legacy data model, where DAV accounts and websites do not have an
// explicit relation.
func groupWebResourcesByHomedir(resources []*accountserver.Resource) {
	// Set the group name to be the 'hostingDir' for sites and DAV
	// accounts. Keep a reference of websites by ID so we can later fix the
	// group for databases too, via their ParentID.
	webs := make(map[string]*accountserver.Resource)
	for _, r := range resources {
ale's avatar
ale committed
427
		switch r.ID.Type() {
428
		case accountserver.ResourceTypeWebsite, accountserver.ResourceTypeDomain:
ale's avatar
ale committed
429
			r.Group = getHostingDir(r.Website.DocumentRoot)
430
			webs[r.ID.String()] = r
ale's avatar
ale committed
431
432
433
434
435
436
		case accountserver.ResourceTypeDAV:
			r.Group = getHostingDir(r.DAV.Homedir)
		}
	}
	// Fix databases in a second pass.
	for _, r := range resources {
ale's avatar
ale committed
437
		if r.ID.Type() == accountserver.ResourceTypeDatabase && !r.ParentID.Empty() {
438
			r.Group = webs[r.ParentID.String()].Group
ale's avatar
ale committed
439
440
441
		}
	}
}