1package openldap
2
3import (
4	"context"
5	"time"
6
7	"github.com/hashicorp/vault/sdk/framework"
8	"github.com/hashicorp/vault/sdk/helper/locksutil"
9	"github.com/hashicorp/vault/sdk/logical"
10	"github.com/hashicorp/vault/sdk/queue"
11)
12
13const (
14	staticRolePath = "static-role/"
15)
16
17func (b *backend) pathListRoles() []*framework.Path {
18	return []*framework.Path{
19		{
20			Pattern: staticRolePath + "?$",
21			Operations: map[logical.Operation]framework.OperationHandler{
22				logical.ListOperation: &framework.PathOperation{
23					Callback: b.pathRoleList,
24				},
25			},
26			HelpSynopsis:    staticRolesListHelpSynopsis,
27			HelpDescription: staticRolesListHelpDescription,
28		},
29	}
30}
31
32func (b *backend) pathRoles() []*framework.Path {
33	return []*framework.Path{
34		{
35			Pattern:        staticRolePath + framework.GenericNameRegex("name"),
36			Fields:         fieldsForType(staticRolePath),
37			ExistenceCheck: b.pathStaticRoleExistenceCheck,
38			Operations: map[logical.Operation]framework.OperationHandler{
39				logical.UpdateOperation: &framework.PathOperation{
40					Callback: b.pathStaticRoleCreateUpdate,
41				},
42				logical.CreateOperation: &framework.PathOperation{
43					Callback: b.pathStaticRoleCreateUpdate,
44				},
45				logical.ReadOperation: &framework.PathOperation{
46					Callback: b.pathStaticRoleRead,
47				},
48				logical.DeleteOperation: &framework.PathOperation{
49					Callback: b.pathStaticRoleDelete,
50				},
51			},
52			HelpSynopsis:    staticRoleHelpSynopsis,
53			HelpDescription: staticRoleHelpDescription,
54		},
55	}
56}
57
58// fieldsForType returns a map of string/FieldSchema items for the given role
59// type. The purpose is to keep the shared fields between dynamic and static
60// roles consistent, and allow for each type to override or provide their own
61// specific fields
62func fieldsForType(roleType string) map[string]*framework.FieldSchema {
63	fields := map[string]*framework.FieldSchema{
64		"name": {
65			Type:        framework.TypeLowerCaseString,
66			Description: "Name of the role",
67		},
68		"username": {
69			Type:        framework.TypeString,
70			Description: "The username/logon name for the entry with which this role will be associated.",
71		},
72		"dn": {
73			Type:        framework.TypeString,
74			Description: "The distinguished name of the entry to manage.",
75		},
76		"ttl": {
77			Type:        framework.TypeDurationSecond,
78			Description: "The time-to-live for the password.",
79		},
80	}
81
82	// Get the fields that are specific to the type of role, and add them to the
83	// common fields. In the future we can add additional for dynamic roles.
84	var typeFields map[string]*framework.FieldSchema
85	switch roleType {
86	case staticRolePath:
87		typeFields = staticFields()
88	}
89
90	for k, v := range typeFields {
91		fields[k] = v
92	}
93
94	return fields
95}
96
97// staticFields returns a map of key and field schema items that are specific
98// only to static roles
99func staticFields() map[string]*framework.FieldSchema {
100	fields := map[string]*framework.FieldSchema{
101		"rotation_period": {
102			Type:        framework.TypeDurationSecond,
103			Description: "Period for automatic credential rotation of the given entry.",
104		},
105	}
106	return fields
107}
108
109func (b *backend) pathStaticRoleExistenceCheck(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) {
110	role, err := b.StaticRole(ctx, req.Storage, data.Get("name").(string))
111	if err != nil {
112		return false, err
113	}
114	return role != nil, nil
115}
116
117func (b *backend) pathStaticRoleDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
118	name := data.Get("name").(string)
119
120	// Grab the exclusive lock
121	lock := locksutil.LockForKey(b.roleLocks, name)
122	lock.Lock()
123	defer lock.Unlock()
124
125	//TODO: Add retry logic
126
127	// Remove the item from the queue
128	_, err := b.popFromRotationQueueByKey(name)
129	if err != nil {
130		return nil, err
131	}
132
133	role, err := b.StaticRole(ctx, req.Storage, name)
134	if err != nil {
135		return nil, err
136	}
137	if role == nil {
138		return nil, nil
139	}
140
141	err = req.Storage.Delete(ctx, staticRolePath+name)
142	if err != nil {
143		return nil, err
144	}
145
146	return nil, nil
147}
148
149func (b *backend) pathStaticRoleRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
150	role, err := b.StaticRole(ctx, req.Storage, d.Get("name").(string))
151	if err != nil {
152		return nil, err
153	}
154	if role == nil {
155		return nil, nil
156	}
157
158	data := map[string]interface{}{
159		"dn":       role.StaticAccount.DN,
160		"username": role.StaticAccount.Username,
161	}
162
163	data["rotation_period"] = role.StaticAccount.RotationPeriod.Seconds()
164	if !role.StaticAccount.LastVaultRotation.IsZero() {
165		data["last_vault_rotation"] = role.StaticAccount.LastVaultRotation
166	}
167
168	return &logical.Response{Data: data}, nil
169}
170
171func (b *backend) pathStaticRoleCreateUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
172	name := data.Get("name").(string)
173
174	// Grab the exclusive lock as well potentially pop and re-push the queue item
175	// for this role
176	lock := locksutil.LockForKey(b.roleLocks, name)
177	lock.Lock()
178	defer lock.Unlock()
179
180	role, err := b.StaticRole(ctx, req.Storage, data.Get("name").(string))
181	if err != nil {
182		return nil, err
183	}
184
185	if role == nil {
186		role = &roleEntry{
187			StaticAccount: &staticAccount{},
188		}
189	}
190
191	dn := data.Get("dn").(string)
192	if dn == "" {
193		return logical.ErrorResponse("dn is a required field to manage a static account"), nil
194	}
195	role.StaticAccount.DN = dn
196
197	username := data.Get("username").(string)
198	if username == "" {
199		return logical.ErrorResponse("username is a required field to manage a static account"), nil
200	}
201	role.StaticAccount.Username = username
202
203	rotationPeriodSecondsRaw, ok := data.GetOk("rotation_period")
204	if !ok {
205		return logical.ErrorResponse("rotation_period is required for static accounts"), nil
206	}
207	rotationPeriodSeconds := rotationPeriodSecondsRaw.(int)
208	if rotationPeriodSeconds < queueTickSeconds {
209		// If rotation frequency is specified the value
210		// must be at least that of the constant queueTickSeconds (5 seconds at
211		// time of writing), otherwise we wont be able to rotate in time
212		return logical.ErrorResponse("rotation_period must be %d seconds or more", queueTickSeconds), nil
213	}
214	role.StaticAccount.RotationPeriod = time.Duration(rotationPeriodSeconds) * time.Second
215
216	// lvr represents the role's LastVaultRotation
217	lvr := role.StaticAccount.LastVaultRotation
218
219	// Only call setStaticAccountPassword if we're creating the role for the
220	// first time
221	switch req.Operation {
222	case logical.CreateOperation:
223		// setStaticAccountPassword calls Storage.Put and saves the role to storage
224		resp, err := b.setStaticAccountPassword(ctx, req.Storage, &setStaticAccountInput{
225			RoleName: name,
226			Role:     role,
227		})
228		if err != nil {
229			return nil, err
230		}
231		// guard against RotationTime not being set or zero-value
232		lvr = resp.RotationTime
233	case logical.UpdateOperation:
234		// store updated Role
235		entry, err := logical.StorageEntryJSON(staticRolePath+name, role)
236		if err != nil {
237			return nil, err
238		}
239		if err := req.Storage.Put(ctx, entry); err != nil {
240			return nil, err
241		}
242
243		// In case this is an update, remove any previous version of the item from
244		// the queue
245
246		//TODO: Add retry logic
247		_, err = b.popFromRotationQueueByKey(name)
248		if err != nil {
249			return nil, err
250		}
251	}
252
253	// Add their rotation to the queue
254	if err := b.pushItem(&queue.Item{
255		Key:      name,
256		Priority: lvr.Add(role.StaticAccount.RotationPeriod).Unix(),
257	}); err != nil {
258		return nil, err
259	}
260
261	return nil, nil
262}
263
264type roleEntry struct {
265	StaticAccount *staticAccount `json:"static_account" mapstructure:"static_account"`
266}
267
268type staticAccount struct {
269	// DN to create or assume management for static accounts
270	DN string `json:"dn"`
271
272	// Username to create or assume management for static accounts
273	Username string `json:"username"`
274
275	// Password is the current password for static accounts. As an input, this is
276	// used/required when trying to assume management of an existing static
277	// account. Return this on credential request if it exists.
278	Password string `json:"password"`
279
280	// LastVaultRotation represents the last time Vault rotated the password
281	LastVaultRotation time.Time `json:"last_vault_rotation"`
282
283	// RotationPeriod is number in seconds between each rotation, effectively a
284	// "time to live". This value is compared to the LastVaultRotation to
285	// determine if a password needs to be rotated
286	RotationPeriod time.Duration `json:"rotation_period"`
287}
288
289// NextRotationTime calculates the next rotation by adding the Rotation Period
290// to the last known vault rotation
291func (s *staticAccount) NextRotationTime() time.Time {
292	return s.LastVaultRotation.Add(s.RotationPeriod)
293}
294
295// PasswordTTL calculates the approximate time remaining until the password is
296// no longer valid. This is approximate because the periodic rotation is only
297// checked approximately every 5 seconds, and each rotation can take a small
298// amount of time to process. This can result in a negative TTL time while the
299// rotation function processes the Static Role and performs the rotation. If the
300// TTL is negative, zero is returned. Users should not trust passwords with a
301// Zero TTL, as they are likely in the process of being rotated and will quickly
302// be invalidated.
303func (s *staticAccount) PasswordTTL() time.Duration {
304	next := s.NextRotationTime()
305	ttl := next.Sub(time.Now()).Round(time.Second)
306	if ttl < 0 {
307		ttl = time.Duration(0)
308	}
309	return ttl
310}
311
312func (b *backend) pathRoleList(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
313	path := staticRolePath
314	entries, err := req.Storage.List(ctx, path)
315	if err != nil {
316		return nil, err
317	}
318
319	return logical.ListResponse(entries), nil
320}
321
322func (b *backend) StaticRole(ctx context.Context, s logical.Storage, roleName string) (*roleEntry, error) {
323	return b.roleAtPath(ctx, s, roleName, staticRolePath)
324}
325
326func (b *backend) roleAtPath(ctx context.Context, s logical.Storage, roleName string, pathPrefix string) (*roleEntry, error) {
327	entry, err := s.Get(ctx, pathPrefix+roleName)
328	if err != nil {
329		return nil, err
330	}
331	if entry == nil {
332		return nil, nil
333	}
334
335	var result roleEntry
336	if err := entry.DecodeJSON(&result); err != nil {
337		return nil, err
338	}
339
340	return &result, nil
341}
342
343const staticRoleHelpSynopsis = `
344Manage the static roles that can be created with this backend.
345`
346
347const staticRoleHelpDescription = `
348This path lets you manage the static roles that can be created with this
349backend. Static Roles are associated with a single LDAP entry, and manage the
350password based on a rotation period, automatically rotating the password.
351
352The "dn" parameter is required and configures the domain name to use when managing
353the existing entry.
354
355The "username" parameter is required and configures the username for the LDAP entry.
356This is helpful to provide a usable name when domain name (DN) isn't used directly for
357authentication.
358
359
360The "rotation_period' parameter is required and configures how often, in seconds, the credentials should be
361automatically rotated by Vault.  The minimum is 5 seconds (5s).
362`
363
364const staticRolesListHelpDescription = `
365List all the static roles being managed by Vault.
366`
367
368const staticRolesListHelpSynopsis = `
369This path lists all the static roles Vault is currently managing in OpenLDAP.
370`
371