package vault import ( "context" "fmt" "strings" "github.com/golang/protobuf/ptypes" "github.com/hashicorp/errwrap" log "github.com/hashicorp/go-hclog" memdb "github.com/hashicorp/go-memdb" "github.com/hashicorp/vault/helper/identity" "github.com/hashicorp/vault/helper/storagepacker" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) const ( groupBucketsPrefix = "packer/group/buckets/" ) func (c *Core) IdentityStore() *IdentityStore { return c.identityStore } // NewIdentityStore creates a new identity store func NewIdentityStore(ctx context.Context, core *Core, config *logical.BackendConfig, logger log.Logger) (*IdentityStore, error) { var err error // Create a new in-memory database for the identity store db, err := memdb.NewMemDB(identityStoreSchema()) if err != nil { return nil, errwrap.Wrapf("failed to create memdb for identity store: {{err}}", err) } iStore := &IdentityStore{ view: config.StorageView, db: db, logger: logger, core: core, } iStore.entityPacker, err = storagepacker.NewStoragePacker(iStore.view, iStore.logger, "") if err != nil { return nil, errwrap.Wrapf("failed to create entity packer: {{err}}", err) } iStore.groupPacker, err = storagepacker.NewStoragePacker(iStore.view, iStore.logger, groupBucketsPrefix) if err != nil { return nil, errwrap.Wrapf("failed to create group packer: {{err}}", err) } iStore.Backend = &framework.Backend{ BackendType: logical.TypeLogical, Paths: framework.PathAppend( entityPaths(iStore), aliasPaths(iStore), groupAliasPaths(iStore), groupPaths(iStore), lookupPaths(iStore), upgradePaths(iStore), ), Invalidate: iStore.Invalidate, } err = iStore.Setup(ctx, config) if err != nil { return nil, err } return iStore, nil } // Invalidate is a callback wherein the backend is informed that the value at // the given key is updated. In identity store's case, it would be the entity // storage entries that get updated. The value needs to be read and MemDB needs // to be updated accordingly. func (i *IdentityStore) Invalidate(ctx context.Context, key string) { i.logger.Debug("invalidate notification received", "key", key) switch { // Check if the key is a storage entry key for an entity bucket case strings.HasPrefix(key, storagepacker.StoragePackerBucketsPrefix): // Get the hash value of the storage bucket entry key bucketKeyHash := i.entityPacker.BucketKeyHashByKey(key) if len(bucketKeyHash) == 0 { i.logger.Error("failed to get the bucket entry key hash") return } // Create a MemDB transaction txn := i.db.Txn(true) defer txn.Abort() // Each entity object in MemDB holds the MD5 hash of the storage // entry key of the entity bucket. Fetch all the entities that // belong to this bucket using the hash value. Remove these entities // from MemDB along with all the aliases of each entity. entitiesFetched, err := i.MemDBEntitiesByBucketEntryKeyHashInTxn(txn, string(bucketKeyHash)) if err != nil { i.logger.Error("failed to fetch entities using the bucket entry key hash", "bucket_entry_key_hash", bucketKeyHash) return } for _, entity := range entitiesFetched { // Delete all the aliases in the entity. This function will also remove // the corresponding alias indexes too. err = i.deleteAliasesInEntityInTxn(txn, entity, entity.Aliases) if err != nil { i.logger.Error("failed to delete aliases in entity", "entity_id", entity.ID, "error", err) return } // Delete the entity using the same transaction err = i.MemDBDeleteEntityByIDInTxn(txn, entity.ID) if err != nil { i.logger.Error("failed to delete entity from MemDB", "entity_id", entity.ID, "error", err) return } } // Get the storage bucket entry bucket, err := i.entityPacker.GetBucket(key) if err != nil { i.logger.Error("failed to refresh entities", "key", key, "error", err) return } // If the underlying entry is nil, it means that this invalidation // notification is for the deletion of the underlying storage entry. At // this point, since all the entities belonging to this bucket are // already removed, there is nothing else to be done. But, if the // storage entry is non-nil, its an indication of an update. In this // case, entities in the updated bucket needs to be reinserted into // MemDB. if bucket != nil { for _, item := range bucket.Items { entity, err := i.parseEntityFromBucketItem(ctx, item) if err != nil { i.logger.Error("failed to parse entity from bucket entry item", "error", err) return } // Only update MemDB and don't touch the storage err = i.upsertEntityInTxn(txn, entity, nil, false) if err != nil { i.logger.Error("failed to update entity in MemDB", "error", err) return } } } txn.Commit() return // Check if the key is a storage entry key for an group bucket case strings.HasPrefix(key, groupBucketsPrefix): // Get the hash value of the storage bucket entry key bucketKeyHash := i.groupPacker.BucketKeyHashByKey(key) if len(bucketKeyHash) == 0 { i.logger.Error("failed to get the bucket entry key hash") return } // Create a MemDB transaction txn := i.db.Txn(true) defer txn.Abort() groupsFetched, err := i.MemDBGroupsByBucketEntryKeyHashInTxn(txn, string(bucketKeyHash)) if err != nil { i.logger.Error("failed to fetch groups using the bucket entry key hash", "bucket_entry_key_hash", bucketKeyHash) return } for _, group := range groupsFetched { // Delete the group using the same transaction err = i.MemDBDeleteGroupByIDInTxn(txn, group.ID) if err != nil { i.logger.Error("failed to delete group from MemDB", "group_id", group.ID, "error", err) return } } // Get the storage bucket entry bucket, err := i.groupPacker.GetBucket(key) if err != nil { i.logger.Error("failed to refresh group", "key", key, "error", err) return } if bucket != nil { for _, item := range bucket.Items { group, err := i.parseGroupFromBucketItem(item) if err != nil { i.logger.Error("failed to parse group from bucket entry item", "error", err) return } // Before updating the group, check if the group exists. If it // does, then delete the group alias from memdb, for the // invalidation would have sent an update. groupFetched, err := i.MemDBGroupByIDInTxn(txn, group.ID, true) if err != nil { i.logger.Error("failed to fetch group from MemDB", "error", err) return } // If the group has an alias remove it from memdb if groupFetched != nil && groupFetched.Alias != nil { err := i.MemDBDeleteAliasByIDInTxn(txn, groupFetched.Alias.ID, true) if err != nil { i.logger.Error("failed to delete old group alias from MemDB", "error", err) return } } // Update MemDB with new group alias information if group.Alias != nil { err = i.MemDBUpsertAliasInTxn(txn, group.Alias, true) if err != nil { i.logger.Error("failed to update group alias in MemDB", "error", err) return } } // Only update MemDB and don't touch the storage err = i.UpsertGroupInTxn(txn, group, false) if err != nil { i.logger.Error("failed to update group in MemDB", "error", err) return } } } txn.Commit() return } } func (i *IdentityStore) parseEntityFromBucketItem(ctx context.Context, item *storagepacker.Item) (*identity.Entity, error) { if item == nil { return nil, fmt.Errorf("nil item") } var entity identity.Entity err := ptypes.UnmarshalAny(item.Message, &entity) if err != nil { return nil, errwrap.Wrapf("failed to decode entity from storage bucket item: {{err}}", err) } return &entity, nil } func (i *IdentityStore) parseGroupFromBucketItem(item *storagepacker.Item) (*identity.Group, error) { if item == nil { return nil, fmt.Errorf("nil item") } var group identity.Group err := ptypes.UnmarshalAny(item.Message, &group) if err != nil { return nil, errwrap.Wrapf("failed to decode group from storage bucket item: {{err}}", err) } return &group, nil } // entityByAliasFactors fetches the entity based on factors of alias, i.e mount // accessor and the alias name. func (i *IdentityStore) entityByAliasFactors(mountAccessor, aliasName string, clone bool) (*identity.Entity, error) { if mountAccessor == "" { return nil, fmt.Errorf("missing mount accessor") } if aliasName == "" { return nil, fmt.Errorf("missing alias name") } txn := i.db.Txn(false) return i.entityByAliasFactorsInTxn(txn, mountAccessor, aliasName, clone) } // entityByAlaisFactorsInTxn fetches the entity based on factors of alias, i.e // mount accessor and the alias name. func (i *IdentityStore) entityByAliasFactorsInTxn(txn *memdb.Txn, mountAccessor, aliasName string, clone bool) (*identity.Entity, error) { if txn == nil { return nil, fmt.Errorf("nil txn") } if mountAccessor == "" { return nil, fmt.Errorf("missing mount accessor") } if aliasName == "" { return nil, fmt.Errorf("missing alias name") } alias, err := i.MemDBAliasByFactorsInTxn(txn, mountAccessor, aliasName, false, false) if err != nil { return nil, err } if alias == nil { return nil, nil } return i.MemDBEntityByAliasIDInTxn(txn, alias.ID, clone) } // CreateOrFetchEntity creates a new entity. This is used by core to // associate each login attempt by an alias to a unified entity in Vault. func (i *IdentityStore) CreateOrFetchEntity(alias *logical.Alias) (*identity.Entity, error) { var entity *identity.Entity var err error if alias == nil { return nil, fmt.Errorf("alias is nil") } if alias.Name == "" { return nil, fmt.Errorf("empty alias name") } mountValidationResp := i.core.router.validateMountByAccessor(alias.MountAccessor) if mountValidationResp == nil { return nil, fmt.Errorf("invalid mount accessor %q", alias.MountAccessor) } if mountValidationResp.MountLocal { return nil, fmt.Errorf("mount_accessor %q is of a local mount", alias.MountAccessor) } if mountValidationResp.MountType != alias.MountType { return nil, fmt.Errorf("mount accessor %q is not a mount of type %q", alias.MountAccessor, alias.MountType) } // Check if an entity already exists for the given alais entity, err = i.entityByAliasFactors(alias.MountAccessor, alias.Name, false) if err != nil { return nil, err } if entity != nil { return entity, nil } i.lock.Lock() defer i.lock.Unlock() // Create a MemDB transaction to update both alias and entity txn := i.db.Txn(true) defer txn.Abort() // Check if an entity was created before acquiring the lock entity, err = i.entityByAliasFactorsInTxn(txn, alias.MountAccessor, alias.Name, false) if err != nil { return nil, err } if entity != nil { return entity, nil } i.logger.Debug("creating a new entity", "alias", alias) entity = &identity.Entity{} err = i.sanitizeEntity(entity) if err != nil { return nil, err } // Create a new alias newAlias := &identity.Alias{ CanonicalID: entity.ID, Name: alias.Name, MountAccessor: alias.MountAccessor, MountPath: mountValidationResp.MountPath, MountType: mountValidationResp.MountType, } err = i.sanitizeAlias(newAlias) if err != nil { return nil, err } // Append the new alias to the new entity entity.Aliases = []*identity.Alias{ newAlias, } // Update MemDB and persist entity object err = i.upsertEntityInTxn(txn, entity, nil, true) if err != nil { return nil, err } txn.Commit() return entity, nil }