Case insensitive identity names (#5404)

* case insensitive identity names

* TestIdentityStore_GroupHierarchyCases

* address review feedback

* Use errwrap.Contains instead of errwrap.ContainsType

* Warn about duplicate names all the time to help fix them

* Address review feedback
This commit is contained in:
Vishal Nayak 2018-10-19 12:47:26 -07:00 committed by GitHub
parent b382517982
commit c677cd0790
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 425 additions and 34 deletions

View File

@ -31,23 +31,31 @@ 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) {
func (i *IdentityStore) resetDB(ctx context.Context) error {
var err error
// Create a new in-memory database for the identity store
db, err := memdb.NewMemDB(identityStoreSchema())
i.db, err = memdb.NewMemDB(identityStoreSchema(!i.disableLowerCasedNames))
if err != nil {
return nil, errwrap.Wrapf("failed to create memdb for identity store: {{err}}", err)
return err
}
return nil
}
func NewIdentityStore(ctx context.Context, core *Core, config *logical.BackendConfig, logger log.Logger) (*IdentityStore, error) {
iStore := &IdentityStore{
view: config.StorageView,
db: db,
logger: logger,
core: core,
}
// Create a memdb instance, which by default, operates on lower cased
// identity names
err := iStore.resetDB(ctx)
if err != nil {
return nil, err
}
entitiesPackerLogger := iStore.logger.Named("storagepacker").Named("entities")
core.AddLogger(entitiesPackerLogger)
groupsPackerLogger := iStore.logger.Named("storagepacker").Named("groups")

View File

@ -181,10 +181,6 @@ func (i *IdentityStore) handleAliasUpdateCommon() framework.OperationFunc {
if entity == nil {
return nil, fmt.Errorf("existing alias is not associated with an entity")
}
if canonicalID == "" || entity.ID == canonicalID {
// Nothing to do
return nil, nil
}
}
resp := &logical.Response{}
@ -255,6 +251,12 @@ func (i *IdentityStore) handleAliasUpdateCommon() framework.OperationFunc {
return nil, err
}
for index, item := range entity.Aliases {
if item.ID == alias.ID {
entity.Aliases[index] = alias
}
}
// 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

View File

@ -2,6 +2,7 @@ package vault
import (
"reflect"
"strings"
"testing"
"github.com/hashicorp/vault/helper/identity"
@ -9,6 +10,89 @@ import (
"github.com/hashicorp/vault/logical"
)
func TestIdentityStore_CaseInsensitiveEntityAliasName(t *testing.T) {
ctx := namespace.RootContext(nil)
i, accessor, _ := testIdentityStoreWithGithubAuth(ctx, t)
// Create an entity
resp, err := i.HandleRequest(ctx, &logical.Request{
Path: "entity",
Operation: logical.UpdateOperation,
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err:%v\nresp: %#v", err, resp)
}
entityID := resp.Data["id"].(string)
testAliasName := "testAliasName"
// Create a case sensitive alias name
resp, err = i.HandleRequest(ctx, &logical.Request{
Path: "entity-alias",
Operation: logical.UpdateOperation,
Data: map[string]interface{}{
"mount_accessor": accessor,
"canonical_id": entityID,
"name": testAliasName,
},
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err:%v\nresp: %#v", err, resp)
}
aliasID := resp.Data["id"].(string)
// Ensure that reading the alias returns case sensitive alias name
resp, err = i.HandleRequest(ctx, &logical.Request{
Path: "entity-alias/id/" + aliasID,
Operation: logical.ReadOperation,
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err:%v\nresp: %#v", err, resp)
}
aliasName := resp.Data["name"].(string)
if aliasName != testAliasName {
t.Fatalf("bad alias name; expected: %q, actual: %q", testAliasName, aliasName)
}
// Overwrite the alias using lower cased alias name. This shouldn't error.
resp, err = i.HandleRequest(ctx, &logical.Request{
Path: "entity-alias/id/" + aliasID,
Operation: logical.UpdateOperation,
Data: map[string]interface{}{
"mount_accessor": accessor,
"canonical_id": entityID,
"name": strings.ToLower(testAliasName),
},
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err:%v\nresp: %#v", err, resp)
}
// Ensure that reading the alias returns lower cased alias name
resp, err = i.HandleRequest(ctx, &logical.Request{
Path: "entity-alias/id/" + aliasID,
Operation: logical.ReadOperation,
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err:%v\nresp: %#v", err, resp)
}
aliasName = resp.Data["name"].(string)
if aliasName != strings.ToLower(testAliasName) {
t.Fatalf("bad alias name; expected: %q, actual: %q", testAliasName, aliasName)
}
// Ensure that there is one entity alias
resp, err = i.HandleRequest(ctx, &logical.Request{
Path: "entity-alias/id",
Operation: logical.ListOperation,
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err:%v\nresp: %#v", err, resp)
}
if len(resp.Data["keys"].([]string)) != 1 {
t.Fatalf("bad length of entity aliases; expected: 1, actual: %d", len(resp.Data["keys"].([]string)))
}
}
// This test is required because MemDB does not take care of ensuring
// uniqueness of indexes that are marked unique.
func TestIdentityStore_AliasSameAliasNames(t *testing.T) {

View File

@ -467,7 +467,7 @@ func (i *IdentityStore) pathEntityNameDelete() framework.OperationFunc {
defer txn.Abort()
// Fetch the entity using its name
entity, err := i.MemDBEntityByNameInTxn(txn, ctx, entityName, true)
entity, err := i.MemDBEntityByNameInTxn(ctx, txn, entityName, true)
if err != nil {
return nil, err
}

View File

@ -5,6 +5,7 @@ import (
"fmt"
"reflect"
"sort"
"strings"
"testing"
uuid "github.com/hashicorp/go-uuid"
@ -14,6 +15,77 @@ import (
"github.com/hashicorp/vault/logical"
)
func TestIdentityStore_CaseInsensitiveEntityName(t *testing.T) {
ctx := namespace.RootContext(nil)
i, _, _ := testIdentityStoreWithGithubAuth(ctx, t)
testEntityName := "testEntityName"
// Create an entity with case sensitive name
resp, err := i.HandleRequest(ctx, &logical.Request{
Path: "entity",
Operation: logical.UpdateOperation,
Data: map[string]interface{}{
"name": testEntityName,
},
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err:%v\nresp: %#v", err, resp)
}
entityID := resp.Data["id"].(string)
// Lookup the entity by ID and check that name returned is case sensitive
resp, err = i.HandleRequest(ctx, &logical.Request{
Path: "entity/id/" + entityID,
Operation: logical.ReadOperation,
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err:%v\nresp: %#v", err, resp)
}
entityName := resp.Data["name"].(string)
if entityName != testEntityName {
t.Fatalf("bad entity name; expected: %q, actual: %q", testEntityName, entityName)
}
// Lookup the entity by case sensitive name
resp, err = i.HandleRequest(ctx, &logical.Request{
Path: "entity/name/" + testEntityName,
Operation: logical.ReadOperation,
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}
entityName = resp.Data["name"].(string)
if entityName != testEntityName {
t.Fatalf("bad entity name; expected: %q, actual: %q", testEntityName, entityName)
}
// Lookup the entity by case insensitive name
resp, err = i.HandleRequest(ctx, &logical.Request{
Path: "entity/name/" + strings.ToLower(testEntityName),
Operation: logical.ReadOperation,
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}
entityName = resp.Data["name"].(string)
if entityName != testEntityName {
t.Fatalf("bad entity name; expected: %q, actual: %q", testEntityName, entityName)
}
// Ensure that there is only one entity
resp, err = i.HandleRequest(ctx, &logical.Request{
Path: "entity/name",
Operation: logical.ListOperation,
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}
if len(resp.Data["keys"].([]string)) != 1 {
t.Fatalf("bad length of entities; expected: 1, actual: %d", len(resp.Data["keys"].([]string)))
}
}
func TestIdentityStore_EntityByName(t *testing.T) {
ctx := namespace.RootContext(nil)
i, _, _ := testIdentityStoreWithGithubAuth(ctx, t)
@ -270,8 +342,8 @@ func TestIdentityStore_EntityCreateUpdate(t *testing.T) {
func TestIdentityStore_CloneImmutability(t *testing.T) {
alias := &identity.Alias{
ID: "testaliasid",
Name: "testaliasname",
ID: "testaliasid",
Name: "testaliasname",
MergedFromCanonicalIDs: []string{"entityid1"},
}

View File

@ -1,6 +1,7 @@
package vault
import (
"strings"
"testing"
credLdap "github.com/hashicorp/vault/builtin/credential/ldap"
@ -10,6 +11,81 @@ import (
"github.com/hashicorp/vault/logical"
)
func TestIdentityStore_CaseInsensitiveGroupAliasName(t *testing.T) {
ctx := namespace.RootContext(nil)
i, accessor, _ := testIdentityStoreWithGithubAuth(ctx, t)
// Create a group
resp, err := i.HandleRequest(ctx, &logical.Request{
Path: "group",
Operation: logical.UpdateOperation,
Data: map[string]interface{}{
"type": "external",
},
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}
groupID := resp.Data["id"].(string)
testAliasName := "testAliasName"
// Create a case sensitive alias name
resp, err = i.HandleRequest(ctx, &logical.Request{
Path: "group-alias",
Operation: logical.UpdateOperation,
Data: map[string]interface{}{
"mount_accessor": accessor,
"canonical_id": groupID,
"name": testAliasName,
},
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err:%v\nresp: %#v", err, resp)
}
aliasID := resp.Data["id"].(string)
// Ensure that reading the alias returns case sensitive alias name
resp, err = i.HandleRequest(ctx, &logical.Request{
Path: "group-alias/id/" + aliasID,
Operation: logical.ReadOperation,
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err:%v\nresp: %#v", err, resp)
}
aliasName := resp.Data["name"].(string)
if aliasName != testAliasName {
t.Fatalf("bad alias name; expected: %q, actual: %q", testAliasName, aliasName)
}
// Overwrite the alias using lower cased alias name. This shouldn't error.
resp, err = i.HandleRequest(ctx, &logical.Request{
Path: "group-alias/id/" + aliasID,
Operation: logical.UpdateOperation,
Data: map[string]interface{}{
"mount_accessor": accessor,
"canonical_id": groupID,
"name": strings.ToLower(testAliasName),
},
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err:%v\nresp: %#v", err, resp)
}
// Ensure that reading the alias returns lower cased alias name
resp, err = i.HandleRequest(ctx, &logical.Request{
Path: "group-alias/id/" + aliasID,
Operation: logical.ReadOperation,
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err:%v\nresp: %#v", err, resp)
}
aliasName = resp.Data["name"].(string)
if aliasName != strings.ToLower(testAliasName) {
t.Fatalf("bad alias name; expected: %q, actual: %q", testAliasName, aliasName)
}
}
func TestIdentityStore_EnsureNoDanglingGroupAlias(t *testing.T) {
err := AddTestCredentialBackend("userpass", credUserpass.Factory)
if err != nil {

View File

@ -3,6 +3,7 @@ package vault
import (
"reflect"
"sort"
"strings"
"testing"
"github.com/go-test/deep"
@ -80,6 +81,77 @@ func TestIdentityStore_MemberGroupIDDelete(t *testing.T) {
}
}
func TestIdentityStore_CaseInsensitiveGroupName(t *testing.T) {
ctx := namespace.RootContext(nil)
i, _, _ := testIdentityStoreWithGithubAuth(ctx, t)
testGroupName := "testGroupName"
// Create an group with case sensitive name
resp, err := i.HandleRequest(ctx, &logical.Request{
Path: "group",
Operation: logical.UpdateOperation,
Data: map[string]interface{}{
"name": testGroupName,
},
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err:%v\nresp: %#v", err, resp)
}
groupID := resp.Data["id"].(string)
// Lookup the group by ID and check that name returned is case sensitive
resp, err = i.HandleRequest(ctx, &logical.Request{
Path: "group/id/" + groupID,
Operation: logical.ReadOperation,
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err:%v\nresp: %#v", err, resp)
}
groupName := resp.Data["name"].(string)
if groupName != testGroupName {
t.Fatalf("bad group name; expected: %q, actual: %q", testGroupName, groupName)
}
// Lookup the group by case sensitive name
resp, err = i.HandleRequest(ctx, &logical.Request{
Path: "group/name/" + testGroupName,
Operation: logical.ReadOperation,
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}
groupName = resp.Data["name"].(string)
if groupName != testGroupName {
t.Fatalf("bad group name; expected: %q, actual: %q", testGroupName, groupName)
}
// Lookup the group by case insensitive name
resp, err = i.HandleRequest(ctx, &logical.Request{
Path: "group/name/" + strings.ToLower(testGroupName),
Operation: logical.ReadOperation,
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}
groupName = resp.Data["name"].(string)
if groupName != testGroupName {
t.Fatalf("bad group name; expected: %q, actual: %q", testGroupName, groupName)
}
// Ensure that there is only one group
resp, err = i.HandleRequest(ctx, &logical.Request{
Path: "group/name",
Operation: logical.ListOperation,
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}
if len(resp.Data["keys"].([]string)) != 1 {
t.Fatalf("bad length of groups; expected: 1, actual: %d", len(resp.Data["keys"].([]string)))
}
}
func TestIdentityStore_GroupByName(t *testing.T) {
ctx := namespace.RootContext(nil)
i, _, _ := testIdentityStoreWithGithubAuth(ctx, t)

View File

@ -13,12 +13,12 @@ const (
groupAliasesTable = "group_aliases"
)
func identityStoreSchema() *memdb.DBSchema {
func identityStoreSchema(lowerCaseName bool) *memdb.DBSchema {
iStoreSchema := &memdb.DBSchema{
Tables: make(map[string]*memdb.TableSchema),
}
schemas := []func() *memdb.TableSchema{
schemas := []func(bool) *memdb.TableSchema{
entitiesTableSchema,
aliasesTableSchema,
groupsTableSchema,
@ -26,7 +26,7 @@ func identityStoreSchema() *memdb.DBSchema {
}
for _, schemaFunc := range schemas {
schema := schemaFunc()
schema := schemaFunc(lowerCaseName)
if _, ok := iStoreSchema.Tables[schema.Name]; ok {
panic(fmt.Sprintf("duplicate table name: %s", schema.Name))
}
@ -36,7 +36,7 @@ func identityStoreSchema() *memdb.DBSchema {
return iStoreSchema
}
func aliasesTableSchema() *memdb.TableSchema {
func aliasesTableSchema(lowerCaseName bool) *memdb.TableSchema {
return &memdb.TableSchema{
Name: entityAliasesTable,
Indexes: map[string]*memdb.IndexSchema{
@ -56,7 +56,8 @@ func aliasesTableSchema() *memdb.TableSchema {
Field: "MountAccessor",
},
&memdb.StringFieldIndex{
Field: "Name",
Field: "Name",
Lowercase: lowerCaseName,
},
},
},
@ -71,7 +72,7 @@ func aliasesTableSchema() *memdb.TableSchema {
}
}
func entitiesTableSchema() *memdb.TableSchema {
func entitiesTableSchema(lowerCaseName bool) *memdb.TableSchema {
return &memdb.TableSchema{
Name: entitiesTable,
Indexes: map[string]*memdb.IndexSchema{
@ -91,7 +92,8 @@ func entitiesTableSchema() *memdb.TableSchema {
Field: "NamespaceID",
},
&memdb.StringFieldIndex{
Field: "Name",
Field: "Name",
Lowercase: lowerCaseName,
},
},
},
@ -120,7 +122,7 @@ func entitiesTableSchema() *memdb.TableSchema {
}
}
func groupsTableSchema() *memdb.TableSchema {
func groupsTableSchema(lowerCaseName bool) *memdb.TableSchema {
return &memdb.TableSchema{
Name: groupsTable,
Indexes: map[string]*memdb.IndexSchema{
@ -140,7 +142,8 @@ func groupsTableSchema() *memdb.TableSchema {
Field: "NamespaceID",
},
&memdb.StringFieldIndex{
Field: "Name",
Field: "Name",
Lowercase: lowerCaseName,
},
},
},
@ -175,7 +178,7 @@ func groupsTableSchema() *memdb.TableSchema {
}
}
func groupAliasesTableSchema() *memdb.TableSchema {
func groupAliasesTableSchema(lowerCaseName bool) *memdb.TableSchema {
return &memdb.TableSchema{
Name: groupAliasesTable,
Indexes: map[string]*memdb.IndexSchema{
@ -195,7 +198,8 @@ func groupAliasesTableSchema() *memdb.TableSchema {
Field: "MountAccessor",
},
&memdb.StringFieldIndex{
Field: "Name",
Field: "Name",
Lowercase: lowerCaseName,
},
},
},

View File

@ -70,6 +70,10 @@ type IdentityStore struct {
// core is the pointer to Vault's core
core *Core
// disableLowerCaseNames indicates whether or not identity artifacts are
// operated case insensitively
disableLowerCasedNames bool
}
type groupDiff struct {

View File

@ -2,6 +2,7 @@ package vault
import (
"context"
"errors"
"fmt"
"strings"
"sync"
@ -19,24 +20,57 @@ import (
"github.com/hashicorp/vault/logical"
)
var (
errDuplicateIdentityName = errors.New("duplicate identity name")
)
func (c *Core) loadIdentityStoreArtifacts(ctx context.Context) error {
var err error
if c.identityStore == nil {
c.logger.Warn("identity store is not setup, skipping loading")
return nil
}
err = c.identityStore.loadEntities(ctx)
loadFunc := func(context.Context) error {
err := c.identityStore.loadEntities(ctx)
if err != nil {
return err
}
return c.identityStore.loadGroups(ctx)
}
// Load everything when memdb is set to operate on lower cased names
err := loadFunc(ctx)
switch {
case err == nil:
// If it succeeds, all is well
return nil
case err != nil && !errwrap.Contains(err, errDuplicateIdentityName.Error()):
return err
}
c.identityStore.logger.Warn("enabling case sensitive identity names")
// Set identity store to operate on case sensitive identity names
c.identityStore.disableLowerCasedNames = true
// Swap the memdb instance by the one which operates on case sensitive
// names, hence obviating the need to unload anything that's already
// loaded.
err = c.identityStore.resetDB(ctx)
if err != nil {
return err
}
err = c.identityStore.loadGroups(ctx)
if err != nil {
return err
}
// Attempt to load identity artifacts once more after memdb is reset to
// accept case sensitive names
return loadFunc(ctx)
}
return nil
func (i *IdentityStore) sanitizeName(name string) string {
if i.disableLowerCasedNames {
return name
}
return strings.ToLower(name)
}
func (i *IdentityStore) loadGroups(ctx context.Context) error {
@ -66,6 +100,18 @@ func (i *IdentityStore) loadGroups(ctx context.Context) error {
continue
}
// Ensure that there are no groups with duplicate names
groupByName, err := i.MemDBGroupByName(ctx, group.Name, false)
if err != nil {
return err
}
if groupByName != nil {
i.logger.Warn(errDuplicateIdentityName.Error(), "group_name", group.Name, "conflicting_group_name", groupByName.Name, "action", "merge the contents of duplicated groups into one and delete the other")
if !i.disableLowerCasedNames {
return errDuplicateIdentityName
}
}
if i.logger.IsDebug() {
i.logger.Debug("loading group", "name", group.Name, "id", group.ID)
}
@ -187,6 +233,18 @@ func (i *IdentityStore) loadEntities(ctx context.Context) error {
continue
}
// Ensure that there are no entities with duplicate names
entityByName, err := i.MemDBEntityByName(ctx, entity.Name, false)
if err != nil {
return nil
}
if entityByName != nil {
i.logger.Warn(errDuplicateIdentityName.Error(), "entity_name", entity.Name, "conflicting_entity_name", entityByName.Name, "action", "merge the duplicate entities into one")
if !i.disableLowerCasedNames {
return errDuplicateIdentityName
}
}
// Only update MemDB and don't hit the storage again
err = i.upsertEntity(ctx, entity, nil, false)
if err != nil {
@ -223,7 +281,9 @@ func (i *IdentityStore) upsertEntityInTxn(ctx context.Context, txn *memdb.Txn, e
return fmt.Errorf("entity is nil")
}
for _, alias := range entity.Aliases {
aliasFactors := make([]string, len(entity.Aliases))
for index, alias := range entity.Aliases {
// Verify that alias is not associated to a different one already
aliasByFactors, err := i.MemDBAliasByFactors(alias.MountAccessor, alias.Name, false, false)
if err != nil {
@ -244,11 +304,20 @@ func (i *IdentityStore) upsertEntityInTxn(ctx context.Context, txn *memdb.Txn, e
return nil
}
if strutil.StrListContains(aliasFactors, i.sanitizeName(alias.Name)+alias.MountAccessor) {
i.logger.Warn(errDuplicateIdentityName.Error(), "alias_name", alias.Name, "mount_accessor", alias.MountAccessor, "entity_name", entity.Name, "action", "delete one of the duplicate aliases")
if !i.disableLowerCasedNames {
return errDuplicateIdentityName
}
}
// Insert or update alias in MemDB using the transaction created above
err = i.MemDBUpsertAliasInTxn(txn, alias, false)
if err != nil {
return err
}
aliasFactors[index] = i.sanitizeName(alias.Name) + alias.MountAccessor
}
// If previous entity is set, update it in MemDB and persist it
@ -583,10 +652,10 @@ func (i *IdentityStore) MemDBEntityByName(ctx context.Context, entityName string
txn := i.db.Txn(false)
return i.MemDBEntityByNameInTxn(txn, ctx, entityName, clone)
return i.MemDBEntityByNameInTxn(ctx, txn, entityName, clone)
}
func (i *IdentityStore) MemDBEntityByNameInTxn(txn *memdb.Txn, ctx context.Context, entityName string, clone bool) (*identity.Entity, error) {
func (i *IdentityStore) MemDBEntityByNameInTxn(ctx context.Context, txn *memdb.Txn, entityName string, clone bool) (*identity.Entity, error) {
if entityName == "" {
return nil, fmt.Errorf("missing entity name")
}