package vault import ( "context" "fmt" "strings" "github.com/golang/protobuf/ptypes" "github.com/hashicorp/errwrap" memdb "github.com/hashicorp/go-memdb" "github.com/hashicorp/vault/helper/identity" "github.com/hashicorp/vault/helper/locksutil" "github.com/hashicorp/vault/helper/storagepacker" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) // aliasPaths returns the API endpoints to operate on aliases. // Following are the paths supported: // entity-alias - To register/modify an alias // entity-alias/id - To read, modify, delete and list aliases based on their ID func aliasPaths(i *IdentityStore) []*framework.Path { return []*framework.Path{ { Pattern: "entity-alias$", Fields: map[string]*framework.FieldSchema{ "id": { Type: framework.TypeString, Description: "ID of the entity alias. If set, updates the corresponding entity alias.", }, // entity_id is deprecated in favor of canonical_id "entity_id": { Type: framework.TypeString, Description: "Entity ID to which this alias belongs to", }, "canonical_id": { Type: framework.TypeString, Description: "Entity ID to which this alias belongs to", }, "mount_accessor": { Type: framework.TypeString, Description: "Mount accessor to which this alias belongs to", }, "name": { Type: framework.TypeString, Description: "Name of the alias", }, "metadata": { Type: framework.TypeKVPairs, Description: `Metadata to be associated with the alias. In CLI, this parameter can be repeated multiple times, and it all gets merged together. For example: vault metadata=key1=value1 metadata=key2=value2 `, }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ logical.UpdateOperation: i.pathAliasRegister(), }, HelpSynopsis: strings.TrimSpace(aliasHelp["alias"][0]), HelpDescription: strings.TrimSpace(aliasHelp["alias"][1]), }, // BC path for identity/entity-alias { Pattern: "alias$", Fields: map[string]*framework.FieldSchema{ "id": { Type: framework.TypeString, Description: "ID of the alias", }, // entity_id is deprecated "entity_id": { Type: framework.TypeString, Description: "Entity ID to which this alias belongs to", }, "canonical_id": { Type: framework.TypeString, Description: "Entity ID to which this alias belongs to", }, "mount_accessor": { Type: framework.TypeString, Description: "Mount accessor to which this alias belongs to", }, "name": { Type: framework.TypeString, Description: "Name of the alias", }, "metadata": { Type: framework.TypeKVPairs, Description: `Metadata to be associated with the alias. In CLI, this parameter can be repeated multiple times, and it all gets merged together. For example: vault metadata=key1=value1 metadata=key2=value2 `, }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ logical.UpdateOperation: i.pathAliasRegister(), }, HelpSynopsis: strings.TrimSpace(aliasHelp["alias"][0]), HelpDescription: strings.TrimSpace(aliasHelp["alias"][1]), }, { Pattern: "entity-alias/id/" + framework.GenericNameRegex("id"), Fields: map[string]*framework.FieldSchema{ "id": { Type: framework.TypeString, Description: "ID of the alias", }, // entity_id is deprecated "entity_id": { Type: framework.TypeString, Description: "Entity ID to which this alias belongs to", }, "canonical_id": { Type: framework.TypeString, Description: "Entity ID to which this alias should be tied to", }, "mount_accessor": { Type: framework.TypeString, Description: "Mount accessor to which this alias belongs to", }, "name": { Type: framework.TypeString, Description: "Name of the alias", }, "metadata": { Type: framework.TypeKVPairs, Description: `Metadata to be associated with the alias. In CLI, this parameter can be repeated multiple times, and it all gets merged together. For example: vault metadata=key1=value1 metadata=key2=value2 `, }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ logical.UpdateOperation: i.pathAliasIDUpdate(), logical.ReadOperation: i.pathAliasIDRead(), logical.DeleteOperation: i.pathAliasIDDelete(), }, HelpSynopsis: strings.TrimSpace(aliasHelp["alias-id"][0]), HelpDescription: strings.TrimSpace(aliasHelp["alias-id"][1]), }, { Pattern: "entity-alias/id/?$", Callbacks: map[logical.Operation]framework.OperationFunc{ logical.ListOperation: i.pathAliasIDList(), }, HelpSynopsis: strings.TrimSpace(aliasHelp["alias-id-list"][0]), HelpDescription: strings.TrimSpace(aliasHelp["alias-id-list"][1]), }, } } // pathAliasRegister is used to register new alias func (i *IdentityStore) pathAliasRegister() framework.OperationFunc { return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { _, ok := d.GetOk("id") if ok { return i.pathAliasIDUpdate()(ctx, req, d) } return i.handleAliasUpdateCommon(req, d, nil) } } // pathAliasIDUpdate is used to update an alias based on the given // alias ID func (i *IdentityStore) pathAliasIDUpdate() framework.OperationFunc { return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { // Get alias id aliasID := d.Get("id").(string) if aliasID == "" { return logical.ErrorResponse("empty alias ID"), nil } alias, err := i.MemDBAliasByID(aliasID, true, false) if err != nil { return nil, err } if alias == nil { return logical.ErrorResponse("invalid alias id"), nil } return i.handleAliasUpdateCommon(req, d, alias) } } // handleAliasUpdateCommon is used to update an alias func (i *IdentityStore) handleAliasUpdateCommon(req *logical.Request, d *framework.FieldData, alias *identity.Alias) (*logical.Response, error) { var err error var newAlias bool var entity *identity.Entity var previousEntity *identity.Entity // Alias will be nil when a new alias is being registered; create a // new struct in that case. if alias == nil { alias = &identity.Alias{} newAlias = true } // Get entity id canonicalID := d.Get("entity_id").(string) if canonicalID == "" { canonicalID = d.Get("canonical_id").(string) } if canonicalID != "" { entity, err = i.MemDBEntityByID(canonicalID, true) if err != nil { return nil, err } if entity == nil { return logical.ErrorResponse("invalid entity ID"), nil } } // Get alias name aliasName := d.Get("name").(string) if aliasName == "" { return logical.ErrorResponse("missing alias name"), nil } mountAccessor := d.Get("mount_accessor").(string) if mountAccessor == "" { return logical.ErrorResponse("missing mount_accessor"), nil } mountValidationResp := i.core.router.validateMountByAccessor(mountAccessor) if mountValidationResp == nil { return logical.ErrorResponse(fmt.Sprintf("invalid mount accessor %q", mountAccessor)), nil } if mountValidationResp.MountLocal { return logical.ErrorResponse(fmt.Sprintf("mount_accessor %q is of a local mount", mountAccessor)), nil } // Get alias metadata metadata, ok, err := d.GetOkErr("metadata") if err != nil { return logical.ErrorResponse(fmt.Sprintf("failed to parse metadata: %v", err)), nil } var aliasMetadata map[string]string if ok { aliasMetadata = metadata.(map[string]string) } aliasByFactors, err := i.MemDBAliasByFactors(mountValidationResp.MountAccessor, aliasName, false, false) if err != nil { return nil, err } resp := &logical.Response{} if newAlias { if aliasByFactors != nil { return logical.ErrorResponse("combination of mount and alias name is already in use"), nil } // If this is an alias being tied to a non-existent entity, create // a new entity for it. if entity == nil { entity = &identity.Entity{ Aliases: []*identity.Alias{ alias, }, } } else { entity.Aliases = append(entity.Aliases, alias) } } else { // Verify that the combination of alias name and mount is not // already tied to a different alias if aliasByFactors != nil && aliasByFactors.ID != alias.ID { return logical.ErrorResponse("combination of mount and alias name is already in use"), nil } // Fetch the entity to which the alias is tied to existingEntity, err := i.MemDBEntityByAliasID(alias.ID, true) if err != nil { return nil, err } if existingEntity == nil { return nil, fmt.Errorf("alias is not associated with an entity") } if entity != nil && entity.ID != existingEntity.ID { // Alias should be transferred from 'existingEntity' to 'entity' for aliasIndex, item := range existingEntity.Aliases { if item.ID == alias.ID { entity.Aliases = append(existingEntity.Aliases[:aliasIndex], existingEntity.Aliases[aliasIndex+1:]...) break } } previousEntity = existingEntity entity.Aliases = append(entity.Aliases, alias) resp.AddWarning(fmt.Sprintf("alias is being transferred from entity %q to %q", existingEntity.ID, entity.ID)) } else { // Update entity with modified alias aliasFound := false for aliasIndex, item := range existingEntity.Aliases { if item.ID == alias.ID { aliasFound = true existingEntity.Aliases[aliasIndex] = alias break } } if !aliasFound { return nil, fmt.Errorf("alias does not exist in entity") } entity = existingEntity } } // ID creation and other validations; This is more useful for new entities // and may not perform anything for the existing entities. Placing the // check here to make the flow common for both new and existing entities. err = i.sanitizeEntity(entity) if err != nil { return nil, err } // Update the fields alias.Name = aliasName alias.Metadata = aliasMetadata alias.MountAccessor = mountValidationResp.MountAccessor // Explicitly set to empty as in the past we incorrectly saved it alias.MountPath = "" alias.MountType = "" // Set the canonical ID in the alias index. This should be done after // sanitizing entity. alias.CanonicalID = entity.ID // ID creation and other validations err = i.sanitizeAlias(alias) if err != nil { return nil, err } // Index entity and its aliases in MemDB and persist entity along with // aliases in storage. If the alias is being transferred over from // one entity to another, previous entity needs to get refreshed in MemDB // and persisted in storage as well. err = i.upsertEntity(entity, previousEntity, true) if err != nil { return nil, err } // Return ID of both alias and entity resp.Data = map[string]interface{}{ "id": alias.ID, "canonical_id": entity.ID, } return resp, nil } // pathAliasIDRead returns the properties of an alias for a given // alias ID func (i *IdentityStore) pathAliasIDRead() framework.OperationFunc { return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { aliasID := d.Get("id").(string) if aliasID == "" { return logical.ErrorResponse("missing alias id"), nil } alias, err := i.MemDBAliasByID(aliasID, false, false) if err != nil { return nil, err } return i.handleAliasReadCommon(alias) } } func (i *IdentityStore) handleAliasReadCommon(alias *identity.Alias) (*logical.Response, error) { if alias == nil { return nil, nil } respData := map[string]interface{}{} respData["id"] = alias.ID respData["canonical_id"] = alias.CanonicalID respData["mount_accessor"] = alias.MountAccessor respData["metadata"] = alias.Metadata respData["name"] = alias.Name respData["merged_from_canonical_ids"] = alias.MergedFromCanonicalIDs if mountValidationResp := i.core.router.validateMountByAccessor(alias.MountAccessor); mountValidationResp != nil { respData["mount_path"] = mountValidationResp.MountPath respData["mount_type"] = mountValidationResp.MountType } // Convert protobuf timestamp into RFC3339 format respData["creation_time"] = ptypes.TimestampString(alias.CreationTime) respData["last_update_time"] = ptypes.TimestampString(alias.LastUpdateTime) return &logical.Response{ Data: respData, }, nil } // pathAliasIDDelete deletes the alias for a given alias ID func (i *IdentityStore) pathAliasIDDelete() framework.OperationFunc { return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { aliasID := d.Get("id").(string) if aliasID == "" { return logical.ErrorResponse("missing alias ID"), nil } // Fetch the alias using its ID alias, err := i.MemDBAliasByID(aliasID, false, false) if err != nil { return nil, err } // If there is no alias for the ID, do nothing if alias == nil { return nil, nil } // Find the entity to which the alias is tied to lockEntity, err := i.MemDBEntityByAliasID(alias.ID, false) if err != nil { return nil, err } // If there is no entity tied to a valid alias, something is wrong if lockEntity == nil { return nil, fmt.Errorf("alias not associated to an entity") } // Acquire the lock to modify the entity storage entry lock := locksutil.LockForKey(i.entityLocks, lockEntity.ID) lock.Lock() defer lock.Unlock() // Create a MemDB transaction to delete entity txn := i.db.Txn(true) defer txn.Abort() // Fetch the alias again after acquiring the lock using the transaction // created above alias, err = i.MemDBAliasByIDInTxn(txn, aliasID, false, false) if err != nil { return nil, err } // If there is no alias for the ID, do nothing if alias == nil { return nil, nil } // Fetch the entity again after acquiring the lock using the transaction // created above entity, err := i.MemDBEntityByAliasIDInTxn(txn, alias.ID, true) if err != nil { return nil, err } // If there is no entity tied to a valid alias, something is wrong if entity == nil { return nil, fmt.Errorf("alias not associated to an entity") } // Lock switching should not end up in this code pointing to different // entities if lockEntity.ID != entity.ID { return nil, fmt.Errorf("operating on an entity to which the lock doesn't belong to") } aliases := []*identity.Alias{ alias, } // Delete alias from the entity object err = i.deleteAliasesInEntityInTxn(txn, entity, aliases) if err != nil { return nil, err } // Update the entity index in the entities table err = i.MemDBUpsertEntityInTxn(txn, entity) if err != nil { return nil, err } // Persist the entity object entityAsAny, err := ptypes.MarshalAny(entity) if err != nil { return nil, err } item := &storagepacker.Item{ ID: entity.ID, Message: entityAsAny, } err = i.entityPacker.PutItem(item) if err != nil { return nil, err } // Committing the transaction *after* successfully updating entity in // storage txn.Commit() return nil, nil } } // pathAliasIDList lists the IDs of all the valid aliases in the identity // store func (i *IdentityStore) pathAliasIDList() framework.OperationFunc { return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { ws := memdb.NewWatchSet() iter, err := i.MemDBAliases(ws, false) if err != nil { return nil, errwrap.Wrapf("failed to fetch iterator for aliases in memdb: {{err}}", err) } var aliasIDs []string aliasInfo := map[string]interface{}{} type mountInfo struct { MountType string MountPath string } mountAccessorMap := map[string]mountInfo{} for { raw := iter.Next() if raw == nil { break } alias := raw.(*identity.Alias) aliasIDs = append(aliasIDs, alias.ID) aliasInfoEntry := map[string]interface{}{ "name": alias.Name, "canonical_id": alias.CanonicalID, "mount_accessor": alias.MountAccessor, } mi, ok := mountAccessorMap[alias.MountAccessor] if ok { aliasInfoEntry["mount_type"] = mi.MountType aliasInfoEntry["mount_path"] = mi.MountPath } else { mi = mountInfo{} if mountValidationResp := i.core.router.validateMountByAccessor(alias.MountAccessor); mountValidationResp != nil { mi.MountType = mountValidationResp.MountType mi.MountPath = mountValidationResp.MountPath aliasInfoEntry["mount_type"] = mi.MountType aliasInfoEntry["mount_path"] = mi.MountPath } mountAccessorMap[alias.MountAccessor] = mi } aliasInfo[alias.ID] = aliasInfoEntry } return logical.ListResponseWithInfo(aliasIDs, aliasInfo), nil } } var aliasHelp = map[string][2]string{ "alias": { "Create a new alias.", "", }, "alias-id": { "Update, read or delete an alias ID.", "", }, "alias-id-list": { "List all the alias IDs.", "", }, }