open-vault/vault/identity_store_aliases.go
Jeff Mitchell 835b355489
Add key information to list endpoints in identity. (#4634)
* Add key information to list endpoints in identity.

Also fixes some bugs from before where we were persisting data that we
should not have been (mount type/path).

* Add cached lookups of real time mount info
2018-05-25 14:34:24 -04:00

481 lines
14 KiB
Go

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/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 <command> <path> 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 <command> <path> 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 <command> <path> 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'
err = i.deleteAliasFromEntity(existingEntity, alias)
if err != nil {
return nil, err
}
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
err = i.updateAliasInEntity(existingEntity, alias)
if err != nil {
return nil, err
}
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
}
return nil, i.deleteAlias(aliasID)
}
}
// 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.",
"",
},
}