1package openldap
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"time"
8
9	"github.com/hashicorp/errwrap"
10	"github.com/hashicorp/go-multierror"
11	"github.com/hashicorp/vault/sdk/framework"
12	"github.com/hashicorp/vault/sdk/helper/base62"
13	"github.com/hashicorp/vault/sdk/helper/consts"
14	"github.com/hashicorp/vault/sdk/helper/locksutil"
15	"github.com/hashicorp/vault/sdk/logical"
16	"github.com/hashicorp/vault/sdk/queue"
17)
18
19const (
20	// Interval to check the queue for items needing rotation
21	queueTickSeconds  = 5
22	queueTickInterval = queueTickSeconds * time.Second
23
24	// WAL storage key used for static account rotations
25	staticWALKey = "staticRotationKey"
26)
27
28// populateQueue loads the priority queue with existing static accounts. This
29// occurs at initialization, after any WAL entries of failed or interrupted
30// rotations have been processed. It lists the roles from storage and searches
31// for any that have an associated static account, then adds them to the
32// priority queue for rotations.
33func (b *backend) populateQueue(ctx context.Context, s logical.Storage) {
34	log := b.Logger()
35	log.Info("populating role rotation queue")
36
37	// Build map of role name / wal entries
38	walMap, err := b.loadStaticWALs(ctx, s)
39	if err != nil {
40		log.Warn("unable to load rotation WALs", "error", err)
41	}
42
43	roles, err := s.List(ctx, staticRolePath)
44	if err != nil {
45		log.Warn("unable to list role for enqueueing", "error", err)
46		return
47	}
48
49	for _, roleName := range roles {
50		select {
51		case <-ctx.Done():
52			log.Info("rotation queue restore cancelled")
53			return
54		default:
55		}
56
57		role, err := b.staticRole(ctx, s, roleName)
58		if err != nil {
59			log.Warn("unable to read static role", "error", err, "role", roleName)
60			continue
61		}
62
63		item := queue.Item{
64			Key:      roleName,
65			Priority: role.StaticAccount.LastVaultRotation.Add(role.StaticAccount.RotationPeriod).Unix(),
66		}
67
68		// Check if role name is in map
69		walEntry := walMap[roleName]
70		if walEntry != nil {
71			// Check walEntry last vault time
72			if !walEntry.LastVaultRotation.IsZero() && walEntry.LastVaultRotation.Before(role.StaticAccount.LastVaultRotation) {
73				// WAL's last vault rotation record is older than the role's data, so
74				// delete and move on
75				if err := framework.DeleteWAL(ctx, s, walEntry.walID); err != nil {
76					log.Warn("unable to delete WAL", "error", err, "WAL ID", walEntry.walID)
77				}
78			} else {
79				log.Info("adjusting priority for Role")
80				item.Value = walEntry.walID
81				item.Priority = time.Now().Unix()
82			}
83		}
84
85		if err := b.pushItem(&item); err != nil {
86			log.Warn("unable to enqueue item", "error", err, "role", roleName)
87		}
88	}
89}
90
91// runTicker kicks off a periodic ticker that invoke the automatic credential
92// rotation method at a determined interval. The default interval is 5 seconds.
93func (b *backend) runTicker(ctx context.Context, s logical.Storage) {
94	b.Logger().Info("starting periodic ticker")
95	tick := time.NewTicker(queueTickInterval)
96	defer tick.Stop()
97	for {
98		select {
99		case <-tick.C:
100			b.rotateCredentials(ctx, s)
101
102		case <-ctx.Done():
103			b.Logger().Info("stopping periodic ticker")
104			return
105		}
106	}
107}
108
109// setCredentialsWAL is used to store information in a WAL that can retry a
110// credential setting or rotation in the event of partial failure.
111type setCredentialsWAL struct {
112	NewPassword string `json:"new_password"`
113	OldPassword string `json:"old_password"`
114	RoleName    string `json:"role_name"`
115	Username    string `json:"username"`
116	DN          string `json:"dn"`
117
118	LastVaultRotation time.Time `json:"last_vault_rotation"`
119
120	walID string
121}
122
123// rotateCredentials sets a new password for a static account. This method is
124// invoked in the runTicker method, which is in it's own go-routine, and invoked
125// periodically (approximately every 5 seconds).
126//
127// This method loops through the priority queue, popping the highest priority
128// item until it encounters the first item that does not yet need rotation,
129// based on the current time.
130func (b *backend) rotateCredentials(ctx context.Context, s logical.Storage) {
131	for b.rotateCredential(ctx, s) {
132	}
133}
134
135func (b *backend) rotateCredential(ctx context.Context, s logical.Storage) bool {
136	// Quit rotating credentials if shutdown has started
137	select {
138	case <-ctx.Done():
139		return false
140	default:
141	}
142	item, err := b.popFromRotationQueue()
143	if err != nil {
144		if err != queue.ErrEmpty {
145			b.Logger().Error("error popping item from queue", "err", err)
146		}
147		return false
148	}
149
150	// Guard against possible nil item
151	if item == nil {
152		return false
153	}
154
155	// Grab the exclusive lock for this Role, to make sure we don't incur and
156	// writes during the rotation process
157	lock := locksutil.LockForKey(b.roleLocks, item.Key)
158	lock.Lock()
159	defer lock.Unlock()
160
161	// Validate the role still exists
162	role, err := b.staticRole(ctx, s, item.Key)
163	if err != nil {
164		b.Logger().Error("unable to load role", "role", item.Key, "error", err)
165		item.Priority = time.Now().Add(10 * time.Second).Unix()
166		if err := b.pushItem(item); err != nil {
167			b.Logger().Error("unable to push item on to queue", "error", err)
168		}
169		return true
170	}
171	if role == nil {
172		b.Logger().Warn("role not found", "role", item.Key, "error", err)
173		return true
174	}
175
176	// If "now" is less than the Item priority, then this item does not need to
177	// be rotated
178	if time.Now().Unix() < item.Priority {
179		if err := b.pushItem(item); err != nil {
180			b.Logger().Error("unable to push item on to queue", "error", err)
181		}
182		// Break out of the for loop
183		return false
184	}
185
186	input := &setStaticAccountInput{
187		RoleName: item.Key,
188		Role:     role,
189	}
190
191	// If there is a WAL entry related to this Role, the corresponding WAL ID
192	// should be stored in the Item's Value field.
193	if walID, ok := item.Value.(string); ok {
194		walEntry, err := b.findStaticWAL(ctx, s, walID)
195		if err != nil {
196			b.Logger().Error("error finding static WAL", "error", err)
197			item.Priority = time.Now().Add(10 * time.Second).Unix()
198			if err := b.pushItem(item); err != nil {
199				b.Logger().Error("unable to push item on to queue", "error", err)
200			}
201		}
202		if walEntry != nil && walEntry.NewPassword != "" {
203			input.Password = walEntry.NewPassword
204			input.WALID = walID
205		}
206	}
207
208	resp, err := b.setStaticAccountPassword(ctx, s, input)
209	if err != nil {
210		b.Logger().Error("unable to rotate credentials in periodic function", "error", err)
211		// Increment the priority enough so that the next call to this method
212		// likely will not attempt to rotate it, as a back-off of sorts
213		item.Priority = time.Now().Add(10 * time.Second).Unix()
214
215		// Preserve the WALID if it was returned
216		if resp != nil && resp.WALID != "" {
217			item.Value = resp.WALID
218		}
219
220		if err := b.pushItem(item); err != nil {
221			b.Logger().Error("unable to push item on to queue", "error", err)
222		}
223		// Go to next item
224		return true
225	}
226
227	lvr := resp.RotationTime
228	if lvr.IsZero() {
229		lvr = time.Now()
230	}
231
232	// Update priority and push updated Item to the queue
233	nextRotation := lvr.Add(role.StaticAccount.RotationPeriod)
234	item.Priority = nextRotation.Unix()
235	if err := b.pushItem(item); err != nil {
236		b.Logger().Warn("unable to push item on to queue", "error", err)
237	}
238	return true
239}
240
241// findStaticWAL loads a WAL entry by ID. If found, only return the WAL if it
242// is of type staticWALKey, otherwise return nil
243func (b *backend) findStaticWAL(ctx context.Context, s logical.Storage, id string) (*setCredentialsWAL, error) {
244	wal, err := framework.GetWAL(ctx, s, id)
245	if err != nil {
246		return nil, err
247	}
248
249	if wal == nil || wal.Kind != staticWALKey {
250		return nil, nil
251	}
252
253	data := wal.Data.(map[string]interface{})
254	walEntry := setCredentialsWAL{
255		walID:       id,
256		NewPassword: data["new_password"].(string),
257		OldPassword: data["old_password"].(string),
258		RoleName:    data["role_name"].(string),
259		Username:    data["username"].(string),
260		DN:          data["dn"].(string),
261	}
262	lvr, err := time.Parse(time.RFC3339, data["last_vault_rotation"].(string))
263	if err != nil {
264		return nil, err
265	}
266	walEntry.LastVaultRotation = lvr
267
268	return &walEntry, nil
269}
270
271type setStaticAccountInput struct {
272	RoleName   string
273	Role       *roleEntry
274	Password   string
275	CreateUser bool
276	WALID      string
277}
278
279type setStaticAccountOutput struct {
280	RotationTime time.Time
281	Password     string
282	// Optional return field, in the event WAL was created and not destroyed
283	// during the operation
284	WALID string
285}
286
287// setStaticAccountPassword sets the password for a static account associated with a
288// Role. This method does many things:
289// - verifies role exists and is in the allowed roles list
290// - loads an existing WAL entry if WALID input is given, otherwise creates a
291// new WAL entry
292// - gets a database connection
293// - accepts an input password, otherwise generates a new one via gRPC to the
294// database plugin
295// - sets new password for the static account
296// - uses WAL for ensuring passwords are not lost if storage to Vault fails
297//
298// This method does not perform any operations on the priority queue. Those
299// tasks must be handled outside of this method.
300func (b *backend) setStaticAccountPassword(ctx context.Context, s logical.Storage, input *setStaticAccountInput) (*setStaticAccountOutput, error) {
301	var merr error
302	if input == nil || input.Role == nil || input.RoleName == "" {
303		return nil, errors.New("input was empty when attempting to set credentials for static account")
304	}
305
306	if _, hasTimeout := ctx.Deadline(); !hasTimeout {
307		var cancel func()
308		ctx, cancel = context.WithTimeout(ctx, defaultCtxTimeout)
309		defer cancel()
310	}
311
312	// Re-use WAL ID if present, otherwise PUT a new WAL
313	output := &setStaticAccountOutput{WALID: input.WALID}
314
315	config, err := readConfig(ctx, s)
316	if err != nil {
317		return nil, err
318	}
319	if config == nil {
320		return nil, errors.New("the config is currently unset")
321	}
322
323	newPassword, err := b.GeneratePassword(ctx, config)
324	if err != nil {
325		return nil, err
326	}
327
328	oldPassword := input.Role.StaticAccount.Password
329
330	// Take out the backend lock since we are swapping out the connection
331	b.Lock()
332	defer b.Unlock()
333
334	if output.WALID == "" {
335		output.WALID, err = framework.PutWAL(ctx, s, staticWALKey, &setCredentialsWAL{
336			RoleName:          input.RoleName,
337			Username:          input.Role.StaticAccount.Username,
338			DN:                input.Role.StaticAccount.DN,
339			NewPassword:       newPassword,
340			OldPassword:       oldPassword,
341			LastVaultRotation: input.Role.StaticAccount.LastVaultRotation,
342		})
343		if err != nil {
344			return output, errwrap.Wrapf("error writing WAL entry: {{err}}", err)
345		}
346	}
347
348	// Update the password remotely.
349	if err := b.client.UpdatePassword(config.LDAP, input.Role.StaticAccount.DN, newPassword); err != nil {
350		return nil, err
351	}
352
353	// Update the password locally.
354	if pwdStoringErr := storePassword(ctx, s, config); pwdStoringErr != nil {
355		// We were unable to store the new password locally. We can't continue in this state because we won't be able
356		// to roll any passwords, including our own to get back into a state of working. So, we need to roll back to
357		// the last password we successfully got into storage.
358		if rollbackErr := b.rollBackPassword(ctx, config, oldPassword); rollbackErr != nil {
359			return nil, fmt.Errorf(`unable to store new password due to %s and unable to return to previous password due
360to %s, configure a new binddn and bindpass to restore openldap function`, pwdStoringErr, rollbackErr)
361		}
362		return nil, fmt.Errorf("unable to update password due to storage err: %s", pwdStoringErr)
363	}
364
365	// Store updated role information
366	// lvr is the known LastVaultRotation
367	lvr := time.Now()
368	input.Role.StaticAccount.LastVaultRotation = lvr
369	input.Role.StaticAccount.Password = newPassword
370	output.RotationTime = lvr
371
372	entry, err := logical.StorageEntryJSON(staticRolePath+input.RoleName, input.Role)
373	if err != nil {
374		return output, err
375	}
376	if err := s.Put(ctx, entry); err != nil {
377		return output, err
378	}
379
380	// Cleanup WAL after successfully rotating and pushing new item on to queue
381	if err := framework.DeleteWAL(ctx, s, output.WALID); err != nil {
382		merr = multierror.Append(merr, err)
383		return output, merr
384	}
385
386	// The WAL has been deleted, return new setStaticAccountOutput without it
387	return &setStaticAccountOutput{RotationTime: lvr}, merr
388}
389
390func (b *backend) GeneratePassword(ctx context.Context, cfg *config) (string, error) {
391	if cfg.PasswordPolicy == "" {
392		if cfg.PasswordLength == 0 {
393			return base62.Random(defaultPasswordLength)
394		}
395		return base62.Random(cfg.PasswordLength)
396	}
397
398	password, err := b.System().GeneratePasswordFromPolicy(ctx, cfg.PasswordPolicy)
399	if err != nil {
400		return "", fmt.Errorf("unable to generate password: %w", err)
401	}
402	return password, nil
403}
404
405// initQueue preforms the necessary checks and initializations needed to preform
406// automatic credential rotation for roles associated with static accounts. This
407// method verifies if a queue is needed (primary server or local mount), and if
408// so initializes the queue and launches a go-routine to periodically invoke a
409// method to preform the rotations.
410//
411// initQueue is invoked by the Factory method in a go-routine. The Factory does
412// not wait for success or failure of it's tasks before continuing. This is to
413// avoid blocking the mount process while loading and evaluating existing roles,
414// etc.
415func (b *backend) initQueue(ctx context.Context, conf *logical.InitializationRequest) {
416	// Verify this mount is on the primary server, or is a local mount. If not, do
417	// not create a queue or launch a ticker. Both processing the WAL list and
418	// populating the queue are done sequentially and before launching a
419	// go-routine to run the periodic ticker.
420	replicationState := b.System().ReplicationState()
421	if (b.System().LocalMount() || !replicationState.HasState(consts.ReplicationPerformanceSecondary)) &&
422		!replicationState.HasState(consts.ReplicationDRSecondary) &&
423		!replicationState.HasState(consts.ReplicationPerformanceStandby) {
424		b.Logger().Info("initializing database rotation queue")
425
426		// Load roles and populate queue with static accounts
427		b.populateQueue(ctx, conf.Storage)
428
429		// Launch ticker
430		go b.runTicker(ctx, conf.Storage)
431	}
432}
433
434// loadStaticWALs reads WAL entries and returns a map of roles and their
435// setCredentialsWAL, if found.
436func (b *backend) loadStaticWALs(ctx context.Context, s logical.Storage) (map[string]*setCredentialsWAL, error) {
437	keys, err := framework.ListWAL(ctx, s)
438	if err != nil {
439		return nil, err
440	}
441	if len(keys) == 0 {
442		b.Logger().Debug("no WAL entries found")
443		return nil, nil
444	}
445
446	walMap := make(map[string]*setCredentialsWAL)
447	// Loop through WAL keys and process any rotation ones
448	for _, walID := range keys {
449		walEntry, err := b.findStaticWAL(ctx, s, walID)
450		if err != nil {
451			b.Logger().Error("error loading static WAL", "id", walID, "error", err)
452			continue
453		}
454		if walEntry == nil {
455			continue
456		}
457
458		// Verify the static role still exists
459		roleName := walEntry.RoleName
460		role, err := b.staticRole(ctx, s, roleName)
461		if err != nil {
462			b.Logger().Warn("unable to read static role", "error", err, "role", roleName)
463			continue
464		}
465		if role == nil || role.StaticAccount == nil {
466			if err := framework.DeleteWAL(ctx, s, walEntry.walID); err != nil {
467				b.Logger().Warn("unable to delete WAL", "error", err, "WAL ID", walEntry.walID)
468			}
469			continue
470		}
471
472		walEntry.walID = walID
473		walMap[walEntry.RoleName] = walEntry
474	}
475	return walMap, nil
476}
477
478// pushItem wraps the internal queue's Push call, to make sure a queue is
479// actually available. This is needed because both runTicker and initQueue
480// operate in go-routines, and could be accessing the queue concurrently
481func (b *backend) pushItem(item *queue.Item) error {
482	b.RLock()
483	defer b.RUnlock()
484
485	if b.credRotationQueue != nil {
486		return b.credRotationQueue.Push(item)
487	}
488
489	b.Logger().Warn("no queue found during push item")
490	return nil
491}
492
493// popFromRotationQueue wraps the internal queue's Pop call, to make sure a queue is
494// actually available. This is needed because both runTicker and initQueue
495// operate in go-routines, and could be accessing the queue concurrently
496func (b *backend) popFromRotationQueue() (*queue.Item, error) {
497	b.RLock()
498	defer b.RUnlock()
499	if b.credRotationQueue != nil {
500		return b.credRotationQueue.Pop()
501	}
502	return nil, queue.ErrEmpty
503}
504
505// popFromRotationQueueByKey wraps the internal queue's PopByKey call, to make sure a queue is
506// actually available. This is needed because both runTicker and initQueue
507// operate in go-routines, and could be accessing the queue concurrently
508func (b *backend) popFromRotationQueueByKey(name string) (*queue.Item, error) {
509	b.RLock()
510	defer b.RUnlock()
511	if b.credRotationQueue != nil {
512		item, err := b.credRotationQueue.PopByKey(name)
513		if err != nil {
514			return nil, err
515		}
516		if item != nil {
517			return item, nil
518		}
519	}
520	return nil, queue.ErrEmpty
521}
522