1package vault
2
3import (
4	"context"
5	"fmt"
6	"strings"
7
8	"github.com/golang/protobuf/ptypes"
9	"github.com/hashicorp/vault/helper/identity"
10	"github.com/hashicorp/vault/helper/namespace"
11	"github.com/hashicorp/vault/helper/storagepacker"
12	"github.com/hashicorp/vault/sdk/framework"
13	"github.com/hashicorp/vault/sdk/logical"
14)
15
16// aliasPaths returns the API endpoints to operate on aliases.
17// Following are the paths supported:
18// entity-alias - To register/modify an alias
19// entity-alias/id - To read, modify, delete and list aliases based on their ID
20func aliasPaths(i *IdentityStore) []*framework.Path {
21	return []*framework.Path{
22		{
23			Pattern: "entity-alias$",
24			Fields: map[string]*framework.FieldSchema{
25				"id": {
26					Type:        framework.TypeString,
27					Description: "ID of the entity alias. If set, updates the corresponding entity alias.",
28				},
29				// entity_id is deprecated in favor of canonical_id
30				"entity_id": {
31					Type: framework.TypeString,
32					Description: `Entity ID to which this alias belongs.
33This field is deprecated, use canonical_id.`,
34				},
35				"canonical_id": {
36					Type:        framework.TypeString,
37					Description: "Entity ID to which this alias belongs",
38				},
39				"mount_accessor": {
40					Type:        framework.TypeString,
41					Description: "Mount accessor to which this alias belongs to; unused for a modify",
42				},
43				"name": {
44					Type:        framework.TypeString,
45					Description: "Name of the alias; unused for a modify",
46				},
47			},
48			Callbacks: map[logical.Operation]framework.OperationFunc{
49				logical.UpdateOperation: i.handleAliasCreateUpdate(),
50			},
51
52			HelpSynopsis:    strings.TrimSpace(aliasHelp["alias"][0]),
53			HelpDescription: strings.TrimSpace(aliasHelp["alias"][1]),
54		},
55		{
56			Pattern: "entity-alias/id/" + framework.GenericNameRegex("id"),
57			Fields: map[string]*framework.FieldSchema{
58				"id": {
59					Type:        framework.TypeString,
60					Description: "ID of the alias",
61				},
62				// entity_id is deprecated
63				"entity_id": {
64					Type: framework.TypeString,
65					Description: `Entity ID to which this alias belongs to.
66This field is deprecated, use canonical_id.`,
67				},
68				"canonical_id": {
69					Type:        framework.TypeString,
70					Description: "Entity ID to which this alias should be tied to",
71				},
72				"mount_accessor": {
73					Type:        framework.TypeString,
74					Description: "(Unused)",
75				},
76				"name": {
77					Type:        framework.TypeString,
78					Description: "(Unused)",
79				},
80			},
81			Callbacks: map[logical.Operation]framework.OperationFunc{
82				logical.UpdateOperation: i.handleAliasCreateUpdate(),
83				logical.ReadOperation:   i.pathAliasIDRead(),
84				logical.DeleteOperation: i.pathAliasIDDelete(),
85			},
86
87			HelpSynopsis:    strings.TrimSpace(aliasHelp["alias-id"][0]),
88			HelpDescription: strings.TrimSpace(aliasHelp["alias-id"][1]),
89		},
90		{
91			Pattern: "entity-alias/id/?$",
92			Callbacks: map[logical.Operation]framework.OperationFunc{
93				logical.ListOperation: i.pathAliasIDList(),
94			},
95
96			HelpSynopsis:    strings.TrimSpace(aliasHelp["alias-id-list"][0]),
97			HelpDescription: strings.TrimSpace(aliasHelp["alias-id-list"][1]),
98		},
99	}
100}
101
102// handleAliasCreateUpdate is used to create or update an alias
103func (i *IdentityStore) handleAliasCreateUpdate() framework.OperationFunc {
104	return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
105		var err error
106
107		ns, err := namespace.FromContext(ctx)
108		if err != nil {
109			return nil, err
110		}
111
112		// Get alias name, if any
113		name := d.Get("name").(string)
114
115		// Get mount accessor, if any
116		mountAccessor := d.Get("mount_accessor").(string)
117
118		// Get ID, if any
119		id := d.Get("id").(string)
120
121		// Get entity id
122		canonicalID := d.Get("canonical_id").(string)
123		if canonicalID == "" {
124			// For backwards compatibility
125			canonicalID = d.Get("entity_id").(string)
126		}
127
128		i.lock.Lock()
129		defer i.lock.Unlock()
130
131		// This block is run if they provided an ID
132		{
133			// If they provide an ID it must be an update. Find the alias, perform
134			// due diligence, call the update function.
135			if id != "" {
136				alias, err := i.MemDBAliasByID(id, true, false)
137				if err != nil {
138					return nil, err
139				}
140				if alias == nil {
141					return logical.ErrorResponse("invalid alias ID provided"), nil
142				}
143				if alias.NamespaceID != ns.ID {
144					return logical.ErrorResponse("cannot modify aliases across namespaces"), logical.ErrPermissionDenied
145				}
146
147				switch {
148				case mountAccessor == "" && name == "":
149					// Just a canonical ID update, maybe
150					if canonicalID == "" {
151						// Nothing to do, so be idempotent
152						return nil, nil
153					}
154
155					name = alias.Name
156					mountAccessor = alias.MountAccessor
157
158				case mountAccessor == "":
159					// No change to mount accessor
160					mountAccessor = alias.MountAccessor
161
162				case name == "":
163					// No change to mount name
164					name = alias.Name
165
166				default:
167					// Both provided
168				}
169
170				return i.handleAliasUpdate(ctx, req, canonicalID, name, mountAccessor, alias)
171			}
172		}
173
174		// If they didn't provide an ID, we must have both accessor and name provided
175		if mountAccessor == "" || name == "" {
176			return logical.ErrorResponse("'id' or 'mount_accessor' and 'name' must be provided"), nil
177		}
178
179		// Look up the alias by factors; if it's found it's an update
180		mountEntry := i.core.router.MatchingMountByAccessor(mountAccessor)
181		if mountEntry == nil {
182			return logical.ErrorResponse(fmt.Sprintf("invalid mount accessor %q", mountAccessor)), nil
183		}
184		if mountEntry.Local {
185			return logical.ErrorResponse(fmt.Sprintf("mount accessor %q is of a local mount", mountAccessor)), nil
186		}
187		if mountEntry.NamespaceID != ns.ID {
188			return logical.ErrorResponse("matching mount is in a different namespace than request"), logical.ErrPermissionDenied
189		}
190		alias, err := i.MemDBAliasByFactors(mountAccessor, name, false, false)
191		if err != nil {
192			return nil, err
193		}
194		if alias != nil {
195			if alias.NamespaceID != ns.ID {
196				return logical.ErrorResponse("cannot modify aliases across namespaces"), logical.ErrPermissionDenied
197			}
198
199			return i.handleAliasUpdate(ctx, req, alias.CanonicalID, name, mountAccessor, alias)
200		}
201
202		// At this point we know it's a new creation request
203		return i.handleAliasCreate(ctx, req, canonicalID, name, mountAccessor)
204	}
205}
206
207func (i *IdentityStore) handleAliasCreate(ctx context.Context, req *logical.Request, canonicalID, name, mountAccessor string) (*logical.Response, error) {
208	ns, err := namespace.FromContext(ctx)
209	if err != nil {
210		return nil, err
211	}
212
213	alias := &identity.Alias{
214		MountAccessor: mountAccessor,
215		Name:          name,
216	}
217	entity := &identity.Entity{}
218
219	// If a canonical ID is provided pull up the entity and make sure we're in
220	// the right NS
221	if canonicalID != "" {
222		entity, err = i.MemDBEntityByID(canonicalID, true)
223		if err != nil {
224			return nil, err
225		}
226		if entity == nil {
227			return logical.ErrorResponse("invalid canonical ID"), nil
228		}
229		if entity.NamespaceID != ns.ID {
230			return logical.ErrorResponse("entity found with 'canonical_id' not in request namespace"), logical.ErrPermissionDenied
231		}
232	}
233
234	entity.Aliases = append(entity.Aliases, alias)
235
236	// ID creation and other validations; This is more useful for new entities
237	// and may not perform anything for the existing entities. Placing the
238	// check here to make the flow common for both new and existing entities.
239	err = i.sanitizeEntity(ctx, entity)
240	if err != nil {
241		return nil, err
242	}
243
244	// Set the canonical ID in the alias index. This should be done after
245	// sanitizing entity in case it's a new entity that didn't have an ID.
246	alias.CanonicalID = entity.ID
247
248	// ID creation and other validations
249	err = i.sanitizeAlias(ctx, alias)
250	if err != nil {
251		return nil, err
252	}
253
254	// Index entity and its aliases in MemDB and persist entity along with
255	// aliases in storage.
256	if err := i.upsertEntity(ctx, entity, nil, true); err != nil {
257		return nil, err
258	}
259
260	// Return ID of both alias and entity
261	return &logical.Response{
262		Data: map[string]interface{}{
263			"id":           alias.ID,
264			"canonical_id": entity.ID,
265		},
266	}, nil
267}
268
269func (i *IdentityStore) handleAliasUpdate(ctx context.Context, req *logical.Request, canonicalID, name, mountAccessor string, alias *identity.Alias) (*logical.Response, error) {
270	if name == alias.Name &&
271		mountAccessor == alias.MountAccessor &&
272		(canonicalID == alias.CanonicalID || canonicalID == "") {
273		// Nothing to do; return nil to be idempotent
274		return nil, nil
275	}
276
277	alias.LastUpdateTime = ptypes.TimestampNow()
278
279	// If we're changing one or the other or both of these, make sure that
280	// there isn't a matching alias already, and make sure it's in the same
281	// namespace.
282	if name != alias.Name || mountAccessor != alias.MountAccessor {
283		// Check here to see if such an alias already exists, if so bail
284		mountEntry := i.core.router.MatchingMountByAccessor(mountAccessor)
285		if mountEntry == nil {
286			return logical.ErrorResponse(fmt.Sprintf("invalid mount accessor %q", mountAccessor)), nil
287		}
288		if mountEntry.Local {
289			return logical.ErrorResponse(fmt.Sprintf("mount_accessor %q is of a local mount", mountAccessor)), nil
290		}
291		if mountEntry.NamespaceID != alias.NamespaceID {
292			return logical.ErrorResponse("given mount accessor is not in the same namespace as the existing alias"), logical.ErrPermissionDenied
293		}
294
295		existingAlias, err := i.MemDBAliasByFactors(mountAccessor, name, false, false)
296		if err != nil {
297			return nil, err
298		}
299		// Bail unless it's just a case change
300		if existingAlias != nil && !strings.EqualFold(existingAlias.Name, name) {
301			return logical.ErrorResponse("alias with combination of mount accessor and name already exists"), nil
302		}
303
304		// Update the values in the alias
305		alias.Name = name
306		alias.MountAccessor = mountAccessor
307	}
308
309	// Get our current entity, which may be the same as the new one if the
310	// canonical ID hasn't changed
311	currentEntity, err := i.MemDBEntityByAliasID(alias.ID, true)
312	if err != nil {
313		return nil, err
314	}
315	if currentEntity == nil {
316		return logical.ErrorResponse("given alias is not associated with an entity"), nil
317	}
318	if currentEntity.NamespaceID != alias.NamespaceID {
319		return logical.ErrorResponse("alias associated with an entity in a different namespace"), logical.ErrPermissionDenied
320	}
321
322	newEntity := currentEntity
323	if canonicalID != "" && canonicalID != alias.CanonicalID {
324		newEntity, err = i.MemDBEntityByID(canonicalID, true)
325		if err != nil {
326			return nil, err
327		}
328		if newEntity == nil {
329			return logical.ErrorResponse("given 'canonical_id' is not associated with an entity"), nil
330		}
331		if newEntity.NamespaceID != alias.NamespaceID {
332			return logical.ErrorResponse("given 'canonical_id' associated with entity in a different namespace from the alias"), logical.ErrPermissionDenied
333		}
334
335		// Update the canonical ID value and move it from the current enitity to the new one
336		alias.CanonicalID = newEntity.ID
337		newEntity.Aliases = append(newEntity.Aliases, alias)
338		for aliasIndex, item := range currentEntity.Aliases {
339			if item.ID == alias.ID {
340				currentEntity.Aliases = append(currentEntity.Aliases[:aliasIndex], currentEntity.Aliases[aliasIndex+1:]...)
341				break
342			}
343		}
344	} else {
345		// If it's not moving we still need to update it in the existing
346		// entity's aliases
347		for aliasIndex, item := range currentEntity.Aliases {
348			if item.ID == alias.ID {
349				currentEntity.Aliases[aliasIndex] = alias
350				break
351			}
352		}
353		// newEntity will be pointing to the same entity; set currentEntity nil
354		// so the upsertCall gets nil for the previous entity as we're only
355		// changing one.
356		currentEntity = nil
357	}
358
359	// Index entity and its aliases in MemDB and persist entity along with
360	// aliases in storage. If the alias is being transferred over from
361	// one entity to another, previous entity needs to get refreshed in MemDB
362	// and persisted in storage as well.
363	if err := i.upsertEntity(ctx, newEntity, currentEntity, true); err != nil {
364		return nil, err
365	}
366
367	// Return ID of both alias and entity
368	return &logical.Response{
369		Data: map[string]interface{}{
370			"id":           alias.ID,
371			"canonical_id": newEntity.ID,
372		},
373	}, nil
374}
375
376// pathAliasIDRead returns the properties of an alias for a given
377// alias ID
378func (i *IdentityStore) pathAliasIDRead() framework.OperationFunc {
379	return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
380		aliasID := d.Get("id").(string)
381		if aliasID == "" {
382			return logical.ErrorResponse("missing alias id"), nil
383		}
384
385		alias, err := i.MemDBAliasByID(aliasID, false, false)
386		if err != nil {
387			return nil, err
388		}
389
390		return i.handleAliasReadCommon(ctx, alias)
391	}
392}
393
394func (i *IdentityStore) handleAliasReadCommon(ctx context.Context, alias *identity.Alias) (*logical.Response, error) {
395	if alias == nil {
396		return nil, nil
397	}
398
399	ns, err := namespace.FromContext(ctx)
400	if err != nil {
401		return nil, err
402	}
403	if ns.ID != alias.NamespaceID {
404		return logical.ErrorResponse("alias and request are in different namespaces"), logical.ErrPermissionDenied
405	}
406
407	respData := map[string]interface{}{}
408	respData["id"] = alias.ID
409	respData["canonical_id"] = alias.CanonicalID
410	respData["mount_accessor"] = alias.MountAccessor
411	respData["metadata"] = alias.Metadata
412	respData["name"] = alias.Name
413	respData["merged_from_canonical_ids"] = alias.MergedFromCanonicalIDs
414	respData["namespace_id"] = alias.NamespaceID
415
416	if mountValidationResp := i.core.router.validateMountByAccessor(alias.MountAccessor); mountValidationResp != nil {
417		respData["mount_path"] = mountValidationResp.MountPath
418		respData["mount_type"] = mountValidationResp.MountType
419	}
420
421	// Convert protobuf timestamp into RFC3339 format
422	respData["creation_time"] = ptypes.TimestampString(alias.CreationTime)
423	respData["last_update_time"] = ptypes.TimestampString(alias.LastUpdateTime)
424
425	return &logical.Response{
426		Data: respData,
427	}, nil
428}
429
430// pathAliasIDDelete deletes the alias for a given alias ID
431func (i *IdentityStore) pathAliasIDDelete() framework.OperationFunc {
432	return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
433		aliasID := d.Get("id").(string)
434		if aliasID == "" {
435			return logical.ErrorResponse("missing alias ID"), nil
436		}
437
438		i.lock.Lock()
439		defer i.lock.Unlock()
440
441		// Create a MemDB transaction to delete entity
442		txn := i.db.Txn(true)
443		defer txn.Abort()
444
445		// Fetch the alias
446		alias, err := i.MemDBAliasByIDInTxn(txn, aliasID, false, false)
447		if err != nil {
448			return nil, err
449		}
450
451		// If there is no alias for the ID, do nothing
452		if alias == nil {
453			return nil, nil
454		}
455
456		ns, err := namespace.FromContext(ctx)
457		if err != nil {
458			return nil, err
459		}
460		if ns.ID != alias.NamespaceID {
461			return logical.ErrorResponse("request and alias are in different namespaces"), logical.ErrPermissionDenied
462		}
463
464		// Fetch the associated entity
465		entity, err := i.MemDBEntityByAliasIDInTxn(txn, alias.ID, true)
466		if err != nil {
467			return nil, err
468		}
469
470		// If there is no entity tied to a valid alias, something is wrong
471		if entity == nil {
472			return nil, fmt.Errorf("alias not associated to an entity")
473		}
474
475		aliases := []*identity.Alias{
476			alias,
477		}
478
479		// Delete alias from the entity object
480		err = i.deleteAliasesInEntityInTxn(txn, entity, aliases)
481		if err != nil {
482			return nil, err
483		}
484
485		// Update the entity index in the entities table
486		err = i.MemDBUpsertEntityInTxn(txn, entity)
487		if err != nil {
488			return nil, err
489		}
490
491		// Persist the entity object
492		entityAsAny, err := ptypes.MarshalAny(entity)
493		if err != nil {
494			return nil, err
495		}
496		item := &storagepacker.Item{
497			ID:      entity.ID,
498			Message: entityAsAny,
499		}
500
501		err = i.entityPacker.PutItem(ctx, item)
502		if err != nil {
503			return nil, err
504		}
505
506		// Committing the transaction *after* successfully updating entity in
507		// storage
508		txn.Commit()
509
510		return nil, nil
511	}
512}
513
514// pathAliasIDList lists the IDs of all the valid aliases in the identity
515// store
516func (i *IdentityStore) pathAliasIDList() framework.OperationFunc {
517	return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
518		return i.handleAliasListCommon(ctx, false)
519	}
520}
521
522var aliasHelp = map[string][2]string{
523	"alias": {
524		"Create a new alias.",
525		"",
526	},
527	"alias-id": {
528		"Update, read or delete an alias ID.",
529		"",
530	},
531	"alias-id-list": {
532		"List all the alias IDs.",
533		"",
534	},
535}
536