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
ale's avatar
ale committed
34
35
	userQuery           *queryConfig
	userResourceQueries []*queryConfig
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,
ale's avatar
ale committed
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
		userQuery: mustCompileQueryConfig(&queryConfig{
			Base:  "uid=${user},ou=People," + base,
			Scope: "base",
		}),
		userResourceQueries: []*queryConfig{
			// Find all resources that are children of the main uid object.
			mustCompileQueryConfig(&queryConfig{
				Base:  "uid=${user},ou=People," + base,
				Scope: "sub",
			}),
			// Find mailing lists, which are nested under a different root.
			mustCompileQueryConfig(&queryConfig{
				Base:   "ou=Lists," + base,
				Filter: "(&(objectClass=mailingList)(listOwner=${user}))",
				Scope:  "one",
			}),
		},
ale's avatar
ale committed
94
		resources: rsrc,
ale's avatar
ale committed
95
	}, nil
ale's avatar
ale committed
96
97
98
99
}

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

ale's avatar
ale committed
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
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
131
132
func (tx *backendTX) getUserDN(user *accountserver.User) string {
	return joinDN("uid="+user.Name, "ou=People", tx.backend.baseDN)
ale's avatar
ale committed
133
134
}

ale's avatar
ale committed
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
// 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
154
// GetUser returns a user.
155
func (tx *backendTX) GetUser(ctx context.Context, username string) (*accountserver.User, error) {
ale's avatar
ale committed
156
157
	// First of all, find the main user object, and just that one.
	vars := map[string]string{"user": username}
158
	result, err := tx.search(ctx, tx.backend.userQuery.searchRequest(vars, nil))
ale's avatar
ale committed
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
	if err != nil {
		if ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) {
			return nil, nil
		}
		return nil, err
	}

	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.
175
176
	for _, query := range tx.backend.userResourceQueries {
		result, err = tx.search(ctx, query.searchRequest(vars, nil))
ale's avatar
ale committed
177
178
179
180
181
182
183
184
185
		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
186
				user.PasswordRecoveryHint = entry.GetAttributeValue(recoveryHintLDAPAttr)
187
188
				user.AppSpecificPasswords = getASPInfo(decodeAppSpecificPasswords(entry.GetAttributeValues(aspLDAPAttr)))
				user.HasEncryptionKeys = (entry.GetAttributeValue(storagePublicKeyLDAPAttr) != "")
ale's avatar
ale committed
189
			}
ale's avatar
ale committed
190

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

	groupWebResourcesByHomedir(user.Resources)

	return user, nil
}

203
func (tx *backendTX) SetUserPassword(ctx context.Context, user *accountserver.User, encryptedPassword string) error {
204
205
206
207
208
209
	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)
	}
210
	return nil
ale's avatar
ale committed
211
212
}

ale's avatar
ale committed
213
214
215
216
217
218
219
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
}

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

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

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

func excludeASPFromList(asps []*appSpecificPassword, id string) []*appSpecificPassword {
	var out []*appSpecificPassword
	for _, asp := range asps {
		if asp.ID != id {
			out = append(out, asp)
		}
250
	}
251
	return out
252
253
}

254
255
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
256

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

264
265
266
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
267
	}
268
269
	return nil
}
ale's avatar
ale committed
270

271
272
273
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))
274
275
	asps = excludeASPFromList(asps, id)
	outASPs := encodeAppSpecificPasswords(asps)
276
277
	tx.setAttr(dn, aspLDAPAttr, outASPs...)
}
ale's avatar
ale committed
278

279
280
281
282
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)
	}
283
	return nil
ale's avatar
ale committed
284
285
}

286
func (tx *backendTX) SetUserTOTPSecret(ctx context.Context, user *accountserver.User, secret string) error {
287
	tx.setAttr(tx.getUserDN(user), totpSecretLDAPAttr, secret)
288
	return nil
ale's avatar
ale committed
289
290
}

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

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

ale's avatar
ale committed
302
303
304
305
func (tx *backendTX) hasResource(ctx context.Context, resourceType, resourceName string) (bool, error) {
	query, err := tx.backend.resources.SearchQuery(resourceType)
	if err != nil {
		return false, err
ale's avatar
ale committed
306
307
308
	}

	// Make a quick LDAP search that only fetches the DN attribute.
309
	result, err := tx.search(ctx, query.searchRequest(map[string]string{
ale's avatar
ale committed
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
		"resource": resourceName,
		"type":     resourceType,
	}, []string{"dn"}))
	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
326
327
328
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
329
330
331
332
333
334
335
		if err != nil || has {
			return has, err
		}
	}
	return false, nil
}

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

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

ale's avatar
ale committed
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
// 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
}

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

392
393
	// 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
394
	for _, attr := range tx.backend.resources.ToLDAP(r) {
395
		tx.setAttr(dn, attr.Type, attr.Vals...)
396
397
	}

398
	return nil
399
400
}

ale's avatar
ale committed
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
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
422
		switch r.ID.Type() {
423
		case accountserver.ResourceTypeWebsite, accountserver.ResourceTypeDomain:
ale's avatar
ale committed
424
			r.Group = getHostingDir(r.Website.DocumentRoot)
425
			webs[r.ID.String()] = r
ale's avatar
ale committed
426
427
428
429
430
431
		case accountserver.ResourceTypeDAV:
			r.Group = getHostingDir(r.DAV.Homedir)
		}
	}
	// Fix databases in a second pass.
	for _, r := range resources {
ale's avatar
ale committed
432
		if r.ID.Type() == accountserver.ResourceTypeDatabase && !r.ParentID.Empty() {
433
			r.Group = webs[r.ParentID.String()].Group
ale's avatar
ale committed
434
435
436
		}
	}
}