1package approle
2
3import (
4	"context"
5	"fmt"
6	"strings"
7	"time"
8
9	"github.com/hashicorp/vault/sdk/framework"
10	"github.com/hashicorp/vault/sdk/helper/cidrutil"
11	"github.com/hashicorp/vault/sdk/helper/parseutil"
12	"github.com/hashicorp/vault/sdk/logical"
13)
14
15func pathLogin(b *backend) *framework.Path {
16	return &framework.Path{
17		Pattern: "login$",
18		Fields: map[string]*framework.FieldSchema{
19			"role_id": {
20				Type:        framework.TypeString,
21				Description: "Unique identifier of the Role. Required to be supplied when the 'bind_secret_id' constraint is set.",
22			},
23			"secret_id": {
24				Type:        framework.TypeString,
25				Default:     "",
26				Description: "SecretID belong to the App role",
27			},
28		},
29		Operations: map[logical.Operation]framework.OperationHandler{
30			logical.UpdateOperation: &framework.PathOperation{
31				Callback: b.pathLoginUpdate,
32			},
33			logical.AliasLookaheadOperation: &framework.PathOperation{
34				Callback: b.pathLoginUpdateAliasLookahead,
35			},
36		},
37		HelpSynopsis:    pathLoginHelpSys,
38		HelpDescription: pathLoginHelpDesc,
39	}
40}
41
42func (b *backend) pathLoginUpdateAliasLookahead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
43	roleID := strings.TrimSpace(data.Get("role_id").(string))
44	if roleID == "" {
45		return nil, fmt.Errorf("missing role_id")
46	}
47
48	return &logical.Response{
49		Auth: &logical.Auth{
50			Alias: &logical.Alias{
51				Name: roleID,
52			},
53		},
54	}, nil
55}
56
57// Returns the Auth object indicating the authentication and authorization information
58// if the credentials provided are validated by the backend.
59func (b *backend) pathLoginUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
60	// RoleID must be supplied during every login
61	roleID := strings.TrimSpace(data.Get("role_id").(string))
62	if roleID == "" {
63		return logical.ErrorResponse("missing role_id"), nil
64	}
65
66	// Look for the storage entry that maps the roleID to role
67	roleIDIndex, err := b.roleIDEntry(ctx, req.Storage, roleID)
68	if err != nil {
69		return nil, err
70	}
71	if roleIDIndex == nil {
72		return logical.ErrorResponse("invalid role ID"), nil
73	}
74
75	roleName := roleIDIndex.Name
76
77	roleLock := b.roleLock(roleName)
78	roleLock.RLock()
79
80	role, err := b.roleEntry(ctx, req.Storage, roleName)
81	roleLock.RUnlock()
82	if err != nil {
83		return nil, err
84	}
85	if role == nil {
86		return logical.ErrorResponse("invalid role ID"), nil
87	}
88
89	metadata := make(map[string]string)
90	var entry *secretIDStorageEntry
91	if role.BindSecretID {
92		secretID := strings.TrimSpace(data.Get("secret_id").(string))
93		if secretID == "" {
94			return logical.ErrorResponse("missing secret_id"), nil
95		}
96
97		secretIDHMAC, err := createHMAC(role.HMACKey, secretID)
98		if err != nil {
99			return nil, fmt.Errorf("failed to create HMAC of secret_id: %w", err)
100		}
101
102		roleNameHMAC, err := createHMAC(role.HMACKey, role.name)
103		if err != nil {
104			return nil, fmt.Errorf("failed to create HMAC of role_name: %w", err)
105		}
106
107		entryIndex := fmt.Sprintf("%s%s/%s", role.SecretIDPrefix, roleNameHMAC, secretIDHMAC)
108
109		secretIDLock := b.secretIDLock(secretIDHMAC)
110		secretIDLock.RLock()
111
112		unlockFunc := secretIDLock.RUnlock
113		defer func() {
114			unlockFunc()
115		}()
116
117		entry, err = b.nonLockedSecretIDStorageEntry(ctx, req.Storage, role.SecretIDPrefix, roleNameHMAC, secretIDHMAC)
118		if err != nil {
119			return nil, err
120		}
121		if entry == nil {
122			return logical.ErrorResponse("invalid secret id"), nil
123		}
124
125		// If a secret ID entry does not have a corresponding accessor
126		// entry, revoke the secret ID immediately
127		accessorEntry, err := b.secretIDAccessorEntry(ctx, req.Storage, entry.SecretIDAccessor, role.SecretIDPrefix)
128		if err != nil {
129			return nil, fmt.Errorf("failed to read secret ID accessor entry: %w", err)
130		}
131		if accessorEntry == nil {
132			// Switch the locks and recheck the conditions
133			secretIDLock.RUnlock()
134			secretIDLock.Lock()
135			unlockFunc = secretIDLock.Unlock
136
137			entry, err = b.nonLockedSecretIDStorageEntry(ctx, req.Storage, role.SecretIDPrefix, roleNameHMAC, secretIDHMAC)
138			if err != nil {
139				return nil, err
140			}
141			if entry == nil {
142				return logical.ErrorResponse("invalid secret id"), nil
143			}
144
145			accessorEntry, err := b.secretIDAccessorEntry(ctx, req.Storage, entry.SecretIDAccessor, role.SecretIDPrefix)
146			if err != nil {
147				return nil, fmt.Errorf("failed to read secret ID accessor entry: %w", err)
148			}
149
150			if accessorEntry == nil {
151				if err := req.Storage.Delete(ctx, entryIndex); err != nil {
152					return nil, fmt.Errorf("error deleting secret ID %q from storage: %w", secretIDHMAC, err)
153				}
154			}
155			return logical.ErrorResponse("invalid secret id"), nil
156		}
157
158		switch {
159		case entry.SecretIDNumUses == 0:
160			//
161			// SecretIDNumUses will be zero only if the usage limit was not set at all,
162			// in which case, the SecretID will remain to be valid as long as it is not
163			// expired.
164			//
165
166			// Ensure that the CIDRs on the secret ID are still a subset of that of
167			// role's
168			err = verifyCIDRRoleSecretIDSubset(entry.CIDRList, role.SecretIDBoundCIDRs)
169			if err != nil {
170				return nil, err
171			}
172
173			// If CIDR restrictions are present on the secret ID, check if the
174			// source IP complies to it
175			if len(entry.CIDRList) != 0 {
176				if req.Connection == nil || req.Connection.RemoteAddr == "" {
177					return nil, fmt.Errorf("failed to get connection information")
178				}
179
180				belongs, err := cidrutil.IPBelongsToCIDRBlocksSlice(req.Connection.RemoteAddr, entry.CIDRList)
181				if !belongs || err != nil {
182					return logical.ErrorResponse(fmt.Errorf(
183						"source address %q unauthorized through CIDR restrictions on the secret ID: %w",
184						req.Connection.RemoteAddr,
185						err,
186					).Error()), nil
187				}
188			}
189		default:
190			//
191			// If the SecretIDNumUses is non-zero, it means that its use-count should be updated
192			// in the storage. Switch the lock from a `read` to a `write` and update
193			// the storage entry.
194			//
195
196			secretIDLock.RUnlock()
197			secretIDLock.Lock()
198			unlockFunc = secretIDLock.Unlock
199
200			// Lock switching may change the data. Refresh the contents.
201			entry, err = b.nonLockedSecretIDStorageEntry(ctx, req.Storage, role.SecretIDPrefix, roleNameHMAC, secretIDHMAC)
202			if err != nil {
203				return nil, err
204			}
205			if entry == nil {
206				return logical.ErrorResponse(fmt.Sprintf("invalid secret_id %q", secretID)), nil
207			}
208
209			// If there exists a single use left, delete the SecretID entry from
210			// the storage but do not fail the validation request. Subsequent
211			// requests to use the same SecretID will fail.
212			if entry.SecretIDNumUses == 1 {
213				// Delete the secret IDs accessor first
214				err = b.deleteSecretIDAccessorEntry(ctx, req.Storage, entry.SecretIDAccessor, role.SecretIDPrefix)
215				if err != nil {
216					return nil, err
217				}
218				err = req.Storage.Delete(ctx, entryIndex)
219				if err != nil {
220					return nil, fmt.Errorf("failed to delete secret ID: %w", err)
221				}
222			} else {
223				// If the use count is greater than one, decrement it and update the last updated time.
224				entry.SecretIDNumUses -= 1
225				entry.LastUpdatedTime = time.Now()
226
227				sEntry, err := logical.StorageEntryJSON(entryIndex, &entry)
228				if err != nil {
229					return nil, err
230				}
231
232				err = req.Storage.Put(ctx, sEntry)
233				if err != nil {
234					return nil, err
235				}
236			}
237
238			// Ensure that the CIDRs on the secret ID are still a subset of that of
239			// role's
240			err = verifyCIDRRoleSecretIDSubset(entry.CIDRList, role.SecretIDBoundCIDRs)
241			if err != nil {
242				return nil, err
243			}
244
245			// If CIDR restrictions are present on the secret ID, check if the
246			// source IP complies to it
247			if len(entry.CIDRList) != 0 {
248				if req.Connection == nil || req.Connection.RemoteAddr == "" {
249					return nil, fmt.Errorf("failed to get connection information")
250				}
251
252				belongs, err := cidrutil.IPBelongsToCIDRBlocksSlice(req.Connection.RemoteAddr, entry.CIDRList)
253				if err != nil || !belongs {
254					return logical.ErrorResponse(
255						fmt.Errorf(
256							"source address %q unauthorized by CIDR restrictions on the secret ID: %w",
257							req.Connection.RemoteAddr,
258							err,
259						).Error()), nil
260				}
261			}
262		}
263
264		metadata = entry.Metadata
265	}
266
267	if len(role.SecretIDBoundCIDRs) != 0 {
268		if req.Connection == nil || req.Connection.RemoteAddr == "" {
269			return nil, fmt.Errorf("failed to get connection information")
270		}
271		belongs, err := cidrutil.IPBelongsToCIDRBlocksSlice(req.Connection.RemoteAddr, role.SecretIDBoundCIDRs)
272		if err != nil || !belongs {
273			return logical.ErrorResponse(
274				fmt.Errorf(
275					"source address %q unauthorized by CIDR restrictions on the role: %w",
276					req.Connection.RemoteAddr,
277					err,
278				).Error()), nil
279		}
280	}
281
282	// Parse the CIDRs we should be binding the token to.
283	tokenBoundCIDRs := role.TokenBoundCIDRs
284	if entry != nil && len(entry.TokenBoundCIDRs) > 0 {
285		tokenBoundCIDRs, err = parseutil.ParseAddrs(entry.TokenBoundCIDRs)
286		if err != nil {
287			return logical.ErrorResponse(err.Error()), nil
288		}
289	}
290
291	// For some reason, if metadata was set to nil while processing secret ID
292	// binding, ensure that it is initialized again to avoid a panic.
293	if metadata == nil {
294		metadata = make(map[string]string)
295	}
296
297	// Always include the role name, for later filtering
298	metadata["role_name"] = role.name
299
300	auth := &logical.Auth{
301		InternalData: map[string]interface{}{
302			"role_name": role.name,
303		},
304		Metadata: metadata,
305		Alias: &logical.Alias{
306			Name:     role.RoleID,
307			Metadata: metadata,
308		},
309	}
310	role.PopulateTokenAuth(auth)
311
312	// Allow for overridden token bound CIDRs
313	auth.BoundCIDRs = tokenBoundCIDRs
314
315	return &logical.Response{
316		Auth: auth,
317	}, nil
318}
319
320// Invoked when the token issued by this backend is attempting a renewal.
321func (b *backend) pathLoginRenew(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
322	roleName := req.Auth.InternalData["role_name"].(string)
323	if roleName == "" {
324		return nil, fmt.Errorf("failed to fetch role_name during renewal")
325	}
326
327	lock := b.roleLock(roleName)
328	lock.RLock()
329	defer lock.RUnlock()
330
331	// Ensure that the Role still exists.
332	role, err := b.roleEntry(ctx, req.Storage, roleName)
333	if err != nil {
334		return nil, fmt.Errorf("failed to validate role %q during renewal: %w", roleName, err)
335	}
336	if role == nil {
337		return nil, fmt.Errorf("role %q does not exist during renewal", roleName)
338	}
339
340	resp := &logical.Response{Auth: req.Auth}
341	resp.Auth.TTL = role.TokenTTL
342	resp.Auth.MaxTTL = role.TokenMaxTTL
343	resp.Auth.Period = role.TokenPeriod
344	return resp, nil
345}
346
347const pathLoginHelpSys = "Issue a token based on the credentials supplied"
348
349const pathLoginHelpDesc = `
350While the credential 'role_id' is required at all times,
351other credentials required depends on the properties App role
352to which the 'role_id' belongs to. The 'bind_secret_id'
353constraint (enabled by default) on the App role requires the
354'secret_id' credential to be presented.
355
356'role_id' is fetched using the 'role/<role_name>/role_id'
357endpoint and 'secret_id' is fetched using the 'role/<role_name>/secret_id'
358endpoint.`
359