[VAULT-3252] Disallow alias creation if entity/accessor combination exists (#12747)
* Disallow alias creation if entity/accessor combination exists * Add changelog * Address review comments * Add handling to aliasUpdate, some field renaming * Update tests to work under new entity-alias constraint * Add check to entity merge, other review fixes * Log duplicated accessors only once * Fix flaky test * Add note about new constraint to docs * Update entity merge warn log
This commit is contained in:
parent
57c568e511
commit
148109b8ed
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:bug
|
||||||
|
core/identity: Disallow entity alias creation/update if a conflicting alias exists for the target entity and mount combination
|
||||||
|
```
|
|
@ -2,6 +2,7 @@ package vault
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -259,7 +260,7 @@ func metricLabelsMatch(t *testing.T, actual []metrics.Label, expected map[string
|
||||||
|
|
||||||
func TestCoreMetrics_EntityGauges(t *testing.T) {
|
func TestCoreMetrics_EntityGauges(t *testing.T) {
|
||||||
ctx := namespace.RootContext(nil)
|
ctx := namespace.RootContext(nil)
|
||||||
is, ghAccessor, core := testIdentityStoreWithGithubAuth(ctx, t)
|
is, ghAccessor, upAccessor, core := testIdentityStoreWithGithubUserpassAuth(ctx, t)
|
||||||
|
|
||||||
// Create an entity
|
// Create an entity
|
||||||
alias1 := &logical.Alias{
|
alias1 := &logical.Alias{
|
||||||
|
@ -278,12 +279,11 @@ func TestCoreMetrics_EntityGauges(t *testing.T) {
|
||||||
Operation: logical.UpdateOperation,
|
Operation: logical.UpdateOperation,
|
||||||
Path: "entity-alias",
|
Path: "entity-alias",
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"name": "githubuser2",
|
"name": "userpassuser",
|
||||||
"canonical_id": entity.ID,
|
"canonical_id": entity.ID,
|
||||||
"mount_accessor": ghAccessor,
|
"mount_accessor": upAccessor,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := is.HandleRequest(ctx, registerReq)
|
resp, err := is.HandleRequest(ctx, registerReq)
|
||||||
if err != nil || (resp != nil && resp.IsError()) {
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
t.Fatalf("err:%v resp:%#v", err, resp)
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
||||||
|
@ -312,18 +312,38 @@ func TestCoreMetrics_EntityGauges(t *testing.T) {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(glv) != 1 {
|
if len(glv) != 2 {
|
||||||
t.Fatalf("Wrong number of gauges %v, expected %v", len(glv), 1)
|
t.Fatalf("Wrong number of gauges %v, expected %v", len(glv), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if glv[0].Value != 2.0 {
|
if glv[0].Value != 1.0 {
|
||||||
t.Errorf("Alias count %v, expected %v", glv[0].Value, 2.0)
|
t.Errorf("Alias count %v, expected %v", glv[0].Value, 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if glv[1].Value != 1.0 {
|
||||||
|
t.Errorf("Alias count %v, expected %v", glv[0].Value, 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort both metrics.Label slices by Name, causing the Label
|
||||||
|
// with Name auth_method to be first in both arrays
|
||||||
|
sort.Slice(glv[0].Labels, func(i, j int) bool { return glv[0].Labels[i].Name < glv[0].Labels[j].Name })
|
||||||
|
sort.Slice(glv[1].Labels, func(i, j int) bool { return glv[1].Labels[i].Name < glv[1].Labels[j].Name })
|
||||||
|
|
||||||
|
// Sort the GaugeLabelValues slice by the Value of the first metric,
|
||||||
|
// in this case auth_method, in each metrics.Label slice
|
||||||
|
sort.Slice(glv, func(i, j int) bool { return glv[i].Labels[0].Value < glv[j].Labels[0].Value })
|
||||||
|
|
||||||
metricLabelsMatch(t, glv[0].Labels,
|
metricLabelsMatch(t, glv[0].Labels,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"namespace": "root",
|
"namespace": "root",
|
||||||
"auth_method": "github",
|
"auth_method": "github",
|
||||||
"mount_point": "auth/github/",
|
"mount_point": "auth/github/",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
metricLabelsMatch(t, glv[1].Labels,
|
||||||
|
map[string]string{
|
||||||
|
"namespace": "root",
|
||||||
|
"auth_method": "userpass",
|
||||||
|
"mount_point": "auth/userpass/",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -267,6 +267,12 @@ func (i *IdentityStore) handleAliasCreate(ctx context.Context, req *logical.Requ
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, currentAlias := range entity.Aliases {
|
||||||
|
if currentAlias.MountAccessor == mountAccessor {
|
||||||
|
return logical.ErrorResponse("Alias already exists for requested entity and mount accessor"), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
entity.Aliases = append(entity.Aliases, alias)
|
entity.Aliases = append(entity.Aliases, alias)
|
||||||
|
|
||||||
// ID creation and other validations; This is more useful for new entities
|
// ID creation and other validations; This is more useful for new entities
|
||||||
|
@ -312,6 +318,29 @@ func (i *IdentityStore) handleAliasUpdate(ctx context.Context, req *logical.Requ
|
||||||
|
|
||||||
alias.LastUpdateTime = ptypes.TimestampNow()
|
alias.LastUpdateTime = ptypes.TimestampNow()
|
||||||
|
|
||||||
|
// Get our current entity, which may be the same as the new one if the
|
||||||
|
// canonical ID hasn't changed
|
||||||
|
currentEntity, err := i.MemDBEntityByAliasID(alias.ID, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if currentEntity == nil {
|
||||||
|
return logical.ErrorResponse("given alias is not associated with an entity"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentEntity.NamespaceID != alias.NamespaceID {
|
||||||
|
return logical.ErrorResponse("alias and entity do not belong to the same namespace"), logical.ErrPermissionDenied
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the accessor is being changed but the entity is not, check if the entity
|
||||||
|
// already has an alias corresponding to the new accessor
|
||||||
|
if mountAccessor != alias.MountAccessor && (canonicalID == "" || canonicalID == alias.CanonicalID) {
|
||||||
|
for _, currentAlias := range currentEntity.Aliases {
|
||||||
|
if currentAlias.MountAccessor == mountAccessor {
|
||||||
|
return logical.ErrorResponse("Alias cannot be updated as the entity already has an alias for the given 'mount_accessor' "), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// If we're changing one or the other or both of these, make sure that
|
// If we're changing one or the other or both of these, make sure that
|
||||||
// there isn't a matching alias already, and make sure it's in the same
|
// there isn't a matching alias already, and make sure it's in the same
|
||||||
// namespace.
|
// namespace.
|
||||||
|
@ -343,18 +372,6 @@ func (i *IdentityStore) handleAliasUpdate(ctx context.Context, req *logical.Requ
|
||||||
alias.MountAccessor = mountAccessor
|
alias.MountAccessor = mountAccessor
|
||||||
alias.CustomMetadata = customMetadata
|
alias.CustomMetadata = customMetadata
|
||||||
}
|
}
|
||||||
// Get our current entity, which may be the same as the new one if the
|
|
||||||
// canonical ID hasn't changed
|
|
||||||
currentEntity, err := i.MemDBEntityByAliasID(alias.ID, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if currentEntity == nil {
|
|
||||||
return logical.ErrorResponse("given alias is not associated with an entity"), nil
|
|
||||||
}
|
|
||||||
if currentEntity.NamespaceID != alias.NamespaceID {
|
|
||||||
return logical.ErrorResponse("alias associated with an entity in a different namespace"), logical.ErrPermissionDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
newEntity := currentEntity
|
newEntity := currentEntity
|
||||||
if canonicalID != "" && canonicalID != alias.CanonicalID {
|
if canonicalID != "" && canonicalID != alias.CanonicalID {
|
||||||
|
@ -369,6 +386,13 @@ func (i *IdentityStore) handleAliasUpdate(ctx context.Context, req *logical.Requ
|
||||||
return logical.ErrorResponse("given 'canonical_id' associated with entity in a different namespace from the alias"), logical.ErrPermissionDenied
|
return logical.ErrorResponse("given 'canonical_id' associated with entity in a different namespace from the alias"), logical.ErrPermissionDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the entity the alias is being updated to, already has an alias for the mount
|
||||||
|
for _, alias := range newEntity.Aliases {
|
||||||
|
if alias.MountAccessor == mountAccessor {
|
||||||
|
return logical.ErrorResponse("Alias cannot be updated as the given entity already has an alias for this mount "), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update the canonical ID value and move it from the current entity to the new one
|
// Update the canonical ID value and move it from the current entity to the new one
|
||||||
alias.CanonicalID = newEntity.ID
|
alias.CanonicalID = newEntity.ID
|
||||||
newEntity.Aliases = append(newEntity.Aliases, alias)
|
newEntity.Aliases = append(newEntity.Aliases, alias)
|
||||||
|
|
|
@ -405,6 +405,218 @@ func TestIdentityStore_AliasUpdate(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test to check that the alias cannot be updated with a new entity
|
||||||
|
// which already has an alias for the mount on the alias to be updated
|
||||||
|
func TestIdentityStore_AliasMove_DuplicateAccessor(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
var resp *logical.Response
|
||||||
|
ctx := namespace.RootContext(nil)
|
||||||
|
is, githubAccessor, _ := testIdentityStoreWithGithubAuth(ctx, t)
|
||||||
|
|
||||||
|
// Create 2 entities and 1 alias on each, against the same github mount
|
||||||
|
resp, err = is.HandleRequest(ctx, &logical.Request{
|
||||||
|
Path: "entity",
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"name": "testentity1",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("bad: err:%v\nresp: %#v", err, resp)
|
||||||
|
}
|
||||||
|
entity1ID := resp.Data["id"].(string)
|
||||||
|
|
||||||
|
alias1Data := map[string]interface{}{
|
||||||
|
"name": "testaliasname1",
|
||||||
|
"mount_accessor": githubAccessor,
|
||||||
|
"canonical_id": entity1ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
aliasReq := &logical.Request{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "entity-alias",
|
||||||
|
Data: alias1Data,
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will create an alias against the requested entity
|
||||||
|
resp, err = is.HandleRequest(ctx, aliasReq)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err = is.HandleRequest(ctx, &logical.Request{
|
||||||
|
Path: "entity",
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"name": "testentity2",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("bad: err:%v\nresp: %#v", err, resp)
|
||||||
|
}
|
||||||
|
entity2ID := resp.Data["id"].(string)
|
||||||
|
|
||||||
|
alias2Data := map[string]interface{}{
|
||||||
|
"name": "testaliasname2",
|
||||||
|
"mount_accessor": githubAccessor,
|
||||||
|
"canonical_id": entity2ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
aliasReq.Data = alias2Data
|
||||||
|
|
||||||
|
// This will create an alias against the requested entity
|
||||||
|
resp, err = is.HandleRequest(ctx, aliasReq)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
||||||
|
}
|
||||||
|
alias2ID := resp.Data["id"].(string)
|
||||||
|
|
||||||
|
// Attempt to update the second alias to point to the first entity
|
||||||
|
updateData := map[string]interface{}{
|
||||||
|
"canonical_id": entity1ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
aliasReq.Data = updateData
|
||||||
|
aliasReq.Path = "entity-alias/id/" + alias2ID
|
||||||
|
resp, err = is.HandleRequest(ctx, aliasReq)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp == nil || !resp.IsError() {
|
||||||
|
t.Fatalf("expected an error as alias on the github accessor exists for testentity1")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that the alias cannot be changed to a mount for which
|
||||||
|
// the entity already has an alias
|
||||||
|
func TestIdentityStore_AliasUpdate_DuplicateAccessor(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
var resp *logical.Response
|
||||||
|
ctx := namespace.RootContext(nil)
|
||||||
|
|
||||||
|
is, ghAccessor, upAccessor, _ := testIdentityStoreWithGithubUserpassAuth(ctx, t)
|
||||||
|
|
||||||
|
// Create 1 entity and 2 aliases on it, one for each mount
|
||||||
|
resp, err = is.HandleRequest(ctx, &logical.Request{
|
||||||
|
Path: "entity",
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"name": "testentity",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("bad: err:%v\nresp: %#v", err, resp)
|
||||||
|
}
|
||||||
|
entityID := resp.Data["id"].(string)
|
||||||
|
|
||||||
|
alias1Data := map[string]interface{}{
|
||||||
|
"name": "testaliasname1",
|
||||||
|
"mount_accessor": ghAccessor,
|
||||||
|
"canonical_id": entityID,
|
||||||
|
}
|
||||||
|
|
||||||
|
aliasReq := &logical.Request{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "entity-alias",
|
||||||
|
Data: alias1Data,
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will create an alias against the requested entity
|
||||||
|
resp, err = is.HandleRequest(ctx, aliasReq)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
alias2Data := map[string]interface{}{
|
||||||
|
"name": "testaliasname2",
|
||||||
|
"mount_accessor": upAccessor,
|
||||||
|
"canonical_id": entityID,
|
||||||
|
}
|
||||||
|
|
||||||
|
aliasReq.Data = alias2Data
|
||||||
|
|
||||||
|
// This will create an alias against the requested entity
|
||||||
|
resp, err = is.HandleRequest(ctx, aliasReq)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
||||||
|
}
|
||||||
|
alias2ID := resp.Data["id"].(string)
|
||||||
|
|
||||||
|
// Attempt to update the userpass mount to point to the github mount
|
||||||
|
updateData := map[string]interface{}{
|
||||||
|
"mount_accessor": ghAccessor,
|
||||||
|
}
|
||||||
|
|
||||||
|
aliasReq.Data = updateData
|
||||||
|
aliasReq.Path = "entity-alias/id/" + alias2ID
|
||||||
|
resp, err = is.HandleRequest(ctx, aliasReq)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp == nil || !resp.IsError() {
|
||||||
|
t.Fatalf("expected an error as an alias on the github accessor already exists for testentity")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that alias creation fails if an alias for the specified mount
|
||||||
|
// and entity has already been created
|
||||||
|
func TestIdentityStore_AliasCreate_DuplicateAccessor(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
var resp *logical.Response
|
||||||
|
ctx := namespace.RootContext(nil)
|
||||||
|
is, githubAccessor, _ := testIdentityStoreWithGithubAuth(ctx, t)
|
||||||
|
|
||||||
|
resp, err = is.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)
|
||||||
|
|
||||||
|
aliasData := map[string]interface{}{
|
||||||
|
"name": "testaliasname",
|
||||||
|
"mount_accessor": githubAccessor,
|
||||||
|
"canonical_id": entityID,
|
||||||
|
}
|
||||||
|
|
||||||
|
aliasReq := &logical.Request{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "entity-alias",
|
||||||
|
Data: aliasData,
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will create an alias against the requested entity
|
||||||
|
resp, err = is.HandleRequest(ctx, aliasReq)
|
||||||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
aliasData["name"] = "testaliasname2"
|
||||||
|
aliasReq = &logical.Request{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "entity-alias",
|
||||||
|
Data: aliasData,
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will try to create a new alias with the same accessor and entity
|
||||||
|
resp, err = is.HandleRequest(ctx, aliasReq)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if resp == nil || !resp.IsError() {
|
||||||
|
t.Fatalf("expected an error as alias already exists for this accessor and entity")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestIdentityStore_AliasUpdate_ByID(t *testing.T) {
|
func TestIdentityStore_AliasUpdate_ByID(t *testing.T) {
|
||||||
var err error
|
var err error
|
||||||
var resp *logical.Response
|
var resp *logical.Response
|
||||||
|
|
|
@ -770,6 +770,15 @@ func (i *IdentityStore) mergeEntity(ctx context.Context, txn *memdb.Txn, toEntit
|
||||||
isPerfSecondaryOrStandby := i.localNode.ReplicationState().HasState(consts.ReplicationPerformanceSecondary) ||
|
isPerfSecondaryOrStandby := i.localNode.ReplicationState().HasState(consts.ReplicationPerformanceSecondary) ||
|
||||||
i.localNode.HAState() == consts.PerfStandby
|
i.localNode.HAState() == consts.PerfStandby
|
||||||
var fromEntityGroups []*identity.Group
|
var fromEntityGroups []*identity.Group
|
||||||
|
|
||||||
|
toEntityAccessors := make(map[string]struct{})
|
||||||
|
|
||||||
|
for _, alias := range toEntity.Aliases {
|
||||||
|
if _, ok := toEntityAccessors[alias.MountAccessor]; !ok {
|
||||||
|
toEntityAccessors[alias.MountAccessor] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, fromEntityID := range sanitizedFromEntityIDs {
|
for _, fromEntityID := range sanitizedFromEntityIDs {
|
||||||
if fromEntityID == toEntity.ID {
|
if fromEntityID == toEntity.ID {
|
||||||
return errors.New("to_entity_id should not be present in from_entity_ids"), nil
|
return errors.New("to_entity_id should not be present in from_entity_ids"), nil
|
||||||
|
@ -799,6 +808,10 @@ func (i *IdentityStore) mergeEntity(ctx context.Context, txn *memdb.Txn, toEntit
|
||||||
return nil, fmt.Errorf("failed to update alias during merge: %w", err)
|
return nil, fmt.Errorf("failed to update alias during merge: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, ok := toEntityAccessors[alias.MountAccessor]; ok {
|
||||||
|
i.logger.Warn("skipping from_entity alias during entity merge as to_entity has an alias with its accessor", "from_entity", fromEntityID, "skipped_alias", alias.ID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
// Add the alias to the desired entity
|
// Add the alias to the desired entity
|
||||||
toEntity.Aliases = append(toEntity.Aliases, alias)
|
toEntity.Aliases = append(toEntity.Aliases, alias)
|
||||||
}
|
}
|
||||||
|
|
|
@ -988,7 +988,7 @@ func TestIdentityStore_MergeEntitiesByID(t *testing.T) {
|
||||||
var resp *logical.Response
|
var resp *logical.Response
|
||||||
|
|
||||||
ctx := namespace.RootContext(nil)
|
ctx := namespace.RootContext(nil)
|
||||||
is, githubAccessor, _ := testIdentityStoreWithGithubAuth(ctx, t)
|
is, githubAccessor, upAccessor, _ := testIdentityStoreWithGithubUserpassAuth(ctx, t)
|
||||||
|
|
||||||
registerData := map[string]interface{}{
|
registerData := map[string]interface{}{
|
||||||
"name": "testentityname2",
|
"name": "testentityname2",
|
||||||
|
@ -1014,13 +1014,7 @@ func TestIdentityStore_MergeEntitiesByID(t *testing.T) {
|
||||||
|
|
||||||
aliasRegisterData3 := map[string]interface{}{
|
aliasRegisterData3 := map[string]interface{}{
|
||||||
"name": "testaliasname3",
|
"name": "testaliasname3",
|
||||||
"mount_accessor": githubAccessor,
|
"mount_accessor": upAccessor,
|
||||||
"metadata": []string{"organization=hashicorp", "team=vault"},
|
|
||||||
}
|
|
||||||
|
|
||||||
aliasRegisterData4 := map[string]interface{}{
|
|
||||||
"name": "testaliasname4",
|
|
||||||
"mount_accessor": githubAccessor,
|
|
||||||
"metadata": []string{"organization=hashicorp", "team=vault"},
|
"metadata": []string{"organization=hashicorp", "team=vault"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1040,7 +1034,6 @@ func TestIdentityStore_MergeEntitiesByID(t *testing.T) {
|
||||||
|
|
||||||
// Set entity ID in alias registration data and register alias
|
// Set entity ID in alias registration data and register alias
|
||||||
aliasRegisterData1["entity_id"] = entityID1
|
aliasRegisterData1["entity_id"] = entityID1
|
||||||
aliasRegisterData2["entity_id"] = entityID1
|
|
||||||
|
|
||||||
aliasReq := &logical.Request{
|
aliasReq := &logical.Request{
|
||||||
Operation: logical.UpdateOperation,
|
Operation: logical.UpdateOperation,
|
||||||
|
@ -1054,13 +1047,6 @@ func TestIdentityStore_MergeEntitiesByID(t *testing.T) {
|
||||||
t.Fatalf("err:%v resp:%#v", err, resp)
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the alias
|
|
||||||
aliasReq.Data = aliasRegisterData2
|
|
||||||
resp, err = is.HandleRequest(ctx, aliasReq)
|
|
||||||
if err != nil || (resp != nil && resp.IsError()) {
|
|
||||||
t.Fatalf("err:%v resp:%#v", err, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
entity1, err := is.MemDBEntityByID(entityID1, false)
|
entity1, err := is.MemDBEntityByID(entityID1, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -1068,8 +1054,8 @@ func TestIdentityStore_MergeEntitiesByID(t *testing.T) {
|
||||||
if entity1 == nil {
|
if entity1 == nil {
|
||||||
t.Fatalf("failed to create entity: %v", err)
|
t.Fatalf("failed to create entity: %v", err)
|
||||||
}
|
}
|
||||||
if len(entity1.Aliases) != 2 {
|
if len(entity1.Aliases) != 1 {
|
||||||
t.Fatalf("bad: number of aliases in entity; expected: 2, actual: %d", len(entity1.Aliases))
|
t.Fatalf("bad: number of aliases in entity; expected: 1, actual: %d", len(entity1.Aliases))
|
||||||
}
|
}
|
||||||
|
|
||||||
entity1GroupReq := &logical.Request{
|
entity1GroupReq := &logical.Request{
|
||||||
|
@ -1094,13 +1080,12 @@ func TestIdentityStore_MergeEntitiesByID(t *testing.T) {
|
||||||
|
|
||||||
entityID2 := resp.Data["id"].(string)
|
entityID2 := resp.Data["id"].(string)
|
||||||
// Set entity ID in alias registration data and register alias
|
// Set entity ID in alias registration data and register alias
|
||||||
aliasRegisterData3["entity_id"] = entityID2
|
aliasRegisterData2["entity_id"] = entityID2
|
||||||
aliasRegisterData4["entity_id"] = entityID2
|
|
||||||
|
|
||||||
aliasReq = &logical.Request{
|
aliasReq = &logical.Request{
|
||||||
Operation: logical.UpdateOperation,
|
Operation: logical.UpdateOperation,
|
||||||
Path: "alias",
|
Path: "alias",
|
||||||
Data: aliasRegisterData3,
|
Data: aliasRegisterData2,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the alias
|
// Register the alias
|
||||||
|
@ -1109,13 +1094,15 @@ func TestIdentityStore_MergeEntitiesByID(t *testing.T) {
|
||||||
t.Fatalf("err:%v resp:%#v", err, resp)
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
aliasRegisterData3["entity_id"] = entityID2
|
||||||
|
|
||||||
|
aliasReq.Data = aliasRegisterData3
|
||||||
|
|
||||||
// Register the alias
|
// Register the alias
|
||||||
aliasReq.Data = aliasRegisterData4
|
|
||||||
resp, err = is.HandleRequest(ctx, aliasReq)
|
resp, err = is.HandleRequest(ctx, aliasReq)
|
||||||
if err != nil || (resp != nil && resp.IsError()) {
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
t.Fatalf("err:%v resp:%#v", err, resp)
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
entity2, err := is.MemDBEntityByID(entityID2, false)
|
entity2, err := is.MemDBEntityByID(entityID2, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -1175,10 +1162,11 @@ func TestIdentityStore_MergeEntitiesByID(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
entity1Aliases := resp.Data["aliases"].([]interface{})
|
entity1Aliases := resp.Data["aliases"].([]interface{})
|
||||||
if len(entity1Aliases) != 4 {
|
if len(entity1Aliases) != 2 {
|
||||||
t.Fatalf("bad: number of aliases in entity; expected: 4, actual: %d", len(entity1Aliases))
|
t.Fatalf("bad: number of aliases in entity; expected: 2, actual: %d", len(entity1Aliases))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
githubAliases := 0
|
||||||
for _, aliasRaw := range entity1Aliases {
|
for _, aliasRaw := range entity1Aliases {
|
||||||
alias := aliasRaw.(map[string]interface{})
|
alias := aliasRaw.(map[string]interface{})
|
||||||
aliasLookedUp, err := is.MemDBAliasByID(alias["id"].(string), false, false)
|
aliasLookedUp, err := is.MemDBAliasByID(alias["id"].(string), false, false)
|
||||||
|
@ -1188,6 +1176,15 @@ func TestIdentityStore_MergeEntitiesByID(t *testing.T) {
|
||||||
if aliasLookedUp == nil {
|
if aliasLookedUp == nil {
|
||||||
t.Fatalf("index for alias id %q is not updated", alias["id"].(string))
|
t.Fatalf("index for alias id %q is not updated", alias["id"].(string))
|
||||||
}
|
}
|
||||||
|
if aliasLookedUp.MountAccessor == githubAccessor {
|
||||||
|
githubAliases += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that only 1 alias for the githubAccessor is present in the merged entity,
|
||||||
|
// as the github alias on entity2 should've been skipped in the merge
|
||||||
|
if githubAliases != 1 {
|
||||||
|
t.Fatalf("Unexcepted number of github aliases in merged entity; expected: 1, actual: %d", githubAliases)
|
||||||
}
|
}
|
||||||
|
|
||||||
entity1Groups := resp.Data["direct_group_ids"].([]string)
|
entity1Groups := resp.Data["direct_group_ids"].([]string)
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/golang/protobuf/ptypes"
|
"github.com/golang/protobuf/ptypes"
|
||||||
uuid "github.com/hashicorp/go-uuid"
|
uuid "github.com/hashicorp/go-uuid"
|
||||||
credGithub "github.com/hashicorp/vault/builtin/credential/github"
|
credGithub "github.com/hashicorp/vault/builtin/credential/github"
|
||||||
|
credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
|
||||||
"github.com/hashicorp/vault/helper/identity"
|
"github.com/hashicorp/vault/helper/identity"
|
||||||
"github.com/hashicorp/vault/helper/namespace"
|
"github.com/hashicorp/vault/helper/namespace"
|
||||||
"github.com/hashicorp/vault/helper/storagepacker"
|
"github.com/hashicorp/vault/helper/storagepacker"
|
||||||
|
@ -187,7 +188,8 @@ func TestIdentityStore_EntityIDPassthrough(t *testing.T) {
|
||||||
|
|
||||||
func TestIdentityStore_CreateOrFetchEntity(t *testing.T) {
|
func TestIdentityStore_CreateOrFetchEntity(t *testing.T) {
|
||||||
ctx := namespace.RootContext(nil)
|
ctx := namespace.RootContext(nil)
|
||||||
is, ghAccessor, _ := testIdentityStoreWithGithubAuth(ctx, t)
|
is, ghAccessor, upAccessor, _ := testIdentityStoreWithGithubUserpassAuth(ctx, t)
|
||||||
|
|
||||||
alias := &logical.Alias{
|
alias := &logical.Alias{
|
||||||
MountType: "github",
|
MountType: "github",
|
||||||
MountAccessor: ghAccessor,
|
MountAccessor: ghAccessor,
|
||||||
|
@ -239,7 +241,7 @@ func TestIdentityStore_CreateOrFetchEntity(t *testing.T) {
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"name": "githubuser2",
|
"name": "githubuser2",
|
||||||
"canonical_id": entity.ID,
|
"canonical_id": entity.ID,
|
||||||
"mount_accessor": ghAccessor,
|
"mount_accessor": upAccessor,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -612,6 +614,48 @@ func testIdentityStoreWithGithubAuthRoot(ctx context.Context, t *testing.T) (*Id
|
||||||
return c.identityStore, meGH.Accessor, c, root
|
return c.identityStore, meGH.Accessor, c, root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testIdentityStoreWithGithubUserpassAuth(ctx context.Context, t *testing.T) (*IdentityStore, string, string, *Core) {
|
||||||
|
// Setup 2 auth backends, github and userpass
|
||||||
|
err := AddTestCredentialBackend("github", credGithub.Factory)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = AddTestCredentialBackend("userpass", credUserpass.Factory)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c, _, _ := TestCoreUnsealed(t)
|
||||||
|
|
||||||
|
githubMe := &MountEntry{
|
||||||
|
Table: credentialTableType,
|
||||||
|
Path: "github/",
|
||||||
|
Type: "github",
|
||||||
|
Description: "github auth",
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.enableCredential(ctx, githubMe)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userpassMe := &MountEntry{
|
||||||
|
Table: credentialTableType,
|
||||||
|
Path: "userpass/",
|
||||||
|
Type: "userpass",
|
||||||
|
Description: "userpass",
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.enableCredential(ctx, userpassMe)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.identityStore, githubMe.Accessor, userpassMe.Accessor, c
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func TestIdentityStore_MetadataKeyRegex(t *testing.T) {
|
func TestIdentityStore_MetadataKeyRegex(t *testing.T) {
|
||||||
key := "validVALID012_-=+/"
|
key := "validVALID012_-=+/"
|
||||||
|
|
||||||
|
|
|
@ -222,6 +222,7 @@ func (i *IdentityStore) loadEntities(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
i.logger.Debug("entities collected", "num_existing", len(existing))
|
i.logger.Debug("entities collected", "num_existing", len(existing))
|
||||||
|
|
||||||
|
duplicatedAccessors := make(map[string]struct{})
|
||||||
// Make the channels used for the worker pool
|
// Make the channels used for the worker pool
|
||||||
broker := make(chan string)
|
broker := make(chan string)
|
||||||
quit := make(chan bool)
|
quit := make(chan bool)
|
||||||
|
@ -341,6 +342,13 @@ func (i *IdentityStore) loadEntities(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mountAccessors := getAccessorsOnDuplicateAliases(entity.Aliases)
|
||||||
|
|
||||||
|
for _, accessor := range mountAccessors {
|
||||||
|
if _, ok := duplicatedAccessors[accessor]; !ok {
|
||||||
|
duplicatedAccessors[accessor] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Only update MemDB and don't hit the storage again
|
// Only update MemDB and don't hit the storage again
|
||||||
err = i.upsertEntity(nsCtx, entity, nil, false)
|
err = i.upsertEntity(nsCtx, entity, nil, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -353,6 +361,18 @@ func (i *IdentityStore) loadEntities(ctx context.Context) error {
|
||||||
// Let all go routines finish
|
// Let all go routines finish
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
|
// Flatten the map into a list of keys, in order to log them
|
||||||
|
duplicatedAccessorsList := make([]string, len(duplicatedAccessors))
|
||||||
|
accessorCounter := 0
|
||||||
|
for accessor := range duplicatedAccessors {
|
||||||
|
duplicatedAccessorsList[accessorCounter] = accessor
|
||||||
|
accessorCounter++
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(duplicatedAccessorsList) > 0 {
|
||||||
|
i.logger.Warn("One or more entities have multiple aliases on the same mount(s), remove duplicates to avoid ACL templating issues", "mount_accessors", duplicatedAccessorsList)
|
||||||
|
}
|
||||||
|
|
||||||
if i.logger.IsInfo() {
|
if i.logger.IsInfo() {
|
||||||
i.logger.Info("entities restored")
|
i.logger.Info("entities restored")
|
||||||
}
|
}
|
||||||
|
@ -360,6 +380,25 @@ func (i *IdentityStore) loadEntities(ctx context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getAccessorsOnDuplicateAliases returns a list of accessors by checking aliases in
|
||||||
|
// the passed in list which belong to the same accessor(s)
|
||||||
|
func getAccessorsOnDuplicateAliases(aliases []*identity.Alias) []string {
|
||||||
|
accessorCounts := make(map[string]int)
|
||||||
|
var mountAccessors []string
|
||||||
|
|
||||||
|
for _, alias := range aliases {
|
||||||
|
accessorCounts[alias.MountAccessor] += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for accessor, accessorCount := range accessorCounts {
|
||||||
|
if accessorCount > 1 {
|
||||||
|
mountAccessors = append(mountAccessors, accessor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mountAccessors
|
||||||
|
}
|
||||||
|
|
||||||
// upsertEntityInTxn either creates or updates an existing entity. The
|
// upsertEntityInTxn either creates or updates an existing entity. The
|
||||||
// operations will be updated in both MemDB and storage. If 'persist' is set to
|
// operations will be updated in both MemDB and storage. If 'persist' is set to
|
||||||
// false, then storage will not be updated. When an alias is transferred from
|
// false, then storage will not be updated. When an alias is transferred from
|
||||||
|
|
|
@ -39,7 +39,8 @@ disabled or moved.
|
||||||
Each user will have multiple accounts with various identity providers. Users
|
Each user will have multiple accounts with various identity providers. Users
|
||||||
can now be mapped as `Entities` and their corresponding accounts with
|
can now be mapped as `Entities` and their corresponding accounts with
|
||||||
authentication providers can be mapped as `Aliases`. In essence, each entity is
|
authentication providers can be mapped as `Aliases`. In essence, each entity is
|
||||||
made up of zero or more aliases.
|
made up of zero or more aliases. An entity cannot have more than one alias for
|
||||||
|
a particular authentication backend.
|
||||||
|
|
||||||
### Entity Management
|
### Entity Management
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue