57c7ecfcd4
* refactor delete utility * refactor delete alias utility * remove MemDBUpsertAlias * Remove MemDBAliasByCanonicalID * remove MemDBAliasesByMetadata * remove MemDBDeleteAliasByID * Remove MemDBUpsertEntity and MemDBEntityByNameInTxn * Remove is.MemDBEntitiesByBucketEntryKeyHash * Remove MemDBEntitiesByBucketEntryKeyHash and MemDBEntityByMergedEntityID * Remove MemDBEntities * Remove validateMemberGroupID * Remove validateEntityID, validateGroupID, deleteAliasFromEntity * Remove updateAliasInEntity * Remove satisfiesMetadataFilters and UpsertGroup * Remove MemDBUpsertGroup * Remove deleteGroupByID * Remove deleleGroupByName * Remove MemDBDeleteGroupByNameInTxn * Remove MemDBGroupsByPolicy and MemDBGroupsByPolicyInTxn * Remove MemDBGroupIterator * Remove MemDBGroupsByBucketEntryKeyHash * Remove deleteGroupAlias * Remove metadata index from entities table * Remove unneeded indexes from entity alias and group alias schema * Remove unneeded index from groups table schema * Fix test * s/entity/lockEntity * Don't expose the memdb instance outside identity store * More txn.Abort() corrections * switch back to deferring abort calls
591 lines
17 KiB
Go
591 lines
17 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/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 <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'
|
|
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.",
|
|
"",
|
|
},
|
|
}
|