package target import ( "context" "sort" "testing" "time" "github.com/hashicorp/boundary/internal/credential/vault" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/oplog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" ) func TestRepository_AddTargetCredentialSources(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) wrapper := db.TestWrapper(t) testKms := kms.TestKms(t, conn, wrapper) iamRepo := iam.TestRepo(t, conn, wrapper) _, staticProj := iam.TestScopes(t, iamRepo) repo, err := NewRepository(rw, rw, testKms) require.NoError(t, err) cs := vault.TestCredentialStores(t, conn, wrapper, staticProj.GetPublicId(), 1)[0] libs := vault.TestCredentialLibraries(t, conn, wrapper, cs.GetPublicId(), 3) require.Len(t, libs, 3) lib1 := libs[0] lib2 := libs[1] lib3 := libs[2] type args struct { targetVersion uint32 credLibIds []string } tests := []struct { name string args args wantCredLibIds []string wantErr bool wantErrCode errors.Code }{ { name: "zero-version", args: args{ targetVersion: 0, }, wantErr: true, wantErrCode: errors.InvalidParameter, }, { name: "no-cred-sources", args: args{ targetVersion: 1, }, wantErr: true, wantErrCode: errors.InvalidParameter, }, { name: "valid-single-source", args: args{ targetVersion: 1, credLibIds: []string{lib1.PublicId}, }, wantCredLibIds: []string{lib1.PublicId}, wantErr: false, }, { name: "valid-multiple-sources", args: args{ targetVersion: 1, credLibIds: []string{lib1.PublicId, lib2.PublicId, lib3.PublicId}, }, wantCredLibIds: []string{lib1.PublicId, lib2.PublicId, lib3.PublicId}, wantErr: false, }, { name: "invalid-source-id", args: args{ targetVersion: 1, credLibIds: []string{lib1.PublicId, lib2.PublicId, lib3.PublicId, "invalid-source-id"}, }, wantErr: true, wantErrCode: errors.NotSpecificIntegrity, }, { name: "bad-version", args: args{ targetVersion: 1000, credLibIds: []string{lib1.PublicId}, }, wantErr: true, wantErrCode: errors.VersionMismatch, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) projTarget := TestTcpTarget(t, conn, staticProj.PublicId, tt.name) gotTarget, _, gotCredSources, err := repo.AddTargetCredentialSources(context.Background(), projTarget.PublicId, tt.args.targetVersion, tt.args.credLibIds) if tt.wantErr { require.Error(err) assert.Truef(errors.Match(errors.T(tt.wantErrCode), err), "unexpected error %s", err.Error()) return } require.NoError(err) assert.Len(gotCredSources, len(tt.wantCredLibIds)) gotCredSourcesMap := map[string]CredentialSource{} for _, s := range gotCredSources { gotCredSourcesMap[s.Id()] = s } for _, id := range tt.wantCredLibIds { assert.NotEmpty(gotCredSourcesMap[id]) } // test to see of the target version update oplog was created err = db.TestVerifyOplog(t, rw, projTarget.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) assert.NoError(err) foundCredSources, err := fetchCredentialSources(context.Background(), rw, projTarget.PublicId) require.NoError(err) assert.Len(foundCredSources, len(gotCredSourcesMap)) for _, s := range foundCredSources { assert.NotEmpty(gotCredSourcesMap[s.Id()]) assert.Equal(projTarget.PublicId, s.TargetId()) } target, _, lookupCredSources, err := repo.LookupTarget(context.Background(), projTarget.PublicId) require.NoError(err) assert.Equal(tt.args.targetVersion+1, target.GetVersion()) assert.Equal(projTarget.GetVersion(), target.GetVersion()-1) assert.True(proto.Equal(gotTarget.(*TcpTarget), target.(*TcpTarget))) assert.Equal(gotCredSources, lookupCredSources) }) } t.Run("add-existing", func(t *testing.T) { assert, require := assert.New(t), require.New(t) projTarget := TestTcpTarget(t, conn, staticProj.PublicId, "add-existing") _, _, gotCredSources, err := repo.AddTargetCredentialSources(context.Background(), projTarget.PublicId, 1, []string{lib1.PublicId}) require.NoError(err) assert.Len(gotCredSources, 1) assert.Equal(lib1.PublicId, gotCredSources[0].Id()) // Adding lib1 again should error _, _, _, err = repo.AddTargetCredentialSources(context.Background(), projTarget.PublicId, 2, []string{lib1.PublicId}) require.Error(err) assert.True(errors.Match(errors.T(errors.NotUnique), err)) // Adding multiple with lib1 in set should error _, _, _, err = repo.AddTargetCredentialSources(context.Background(), projTarget.PublicId, 2, []string{lib3.PublicId, lib2.PublicId, lib1.PublicId}) require.Error(err) assert.True(errors.Match(errors.T(errors.NotUnique), err)) // Previous transactions should have been rolled back and only lib1 should be associated gotCredSources, err = fetchCredentialSources(context.Background(), rw, projTarget.PublicId) require.NoError(err) assert.Len(gotCredSources, 1) assert.Equal(lib1.PublicId, gotCredSources[0].Id()) }) t.Run("target-not-found", func(t *testing.T) { assert, require := assert.New(t), require.New(t) _, _, _, err := repo.AddTargetCredentialSources(context.Background(), "fake-target-id", 1, []string{lib1.PublicId}) require.Error(err) assert.Truef(errors.Match(errors.T(errors.RecordNotFound), err), "unexpected error %s", err.Error()) }) } func TestRepository_DeleteTargetCredentialSources(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) wrapper := db.TestWrapper(t) testKms := kms.TestKms(t, conn, wrapper) iamRepo := iam.TestRepo(t, conn, wrapper) _, proj := iam.TestScopes(t, iamRepo) repo, err := NewRepository(rw, rw, testKms) require.NoError(t, err) type args struct { targetIdOverride *string targetVersionOverride *uint32 createCnt int deleteCnt int } tests := []struct { name string args args wantRowsDeleted int wantErr bool wantErrCode errors.Code }{ { name: "valid", args: args{ createCnt: 5, deleteCnt: 5, }, wantRowsDeleted: 5, wantErr: false, }, { name: "valid-keeping-some", args: args{ createCnt: 5, deleteCnt: 2, }, wantRowsDeleted: 2, wantErr: false, }, { name: "no-deletes", args: args{ createCnt: 5, }, wantRowsDeleted: 0, wantErr: true, wantErrCode: errors.InvalidParameter, }, { name: "not-found", args: args{ targetIdOverride: func() *string { id := testId(t); return &id }(), createCnt: 5, deleteCnt: 5, }, wantRowsDeleted: 0, wantErr: true, wantErrCode: errors.RecordNotFound, }, { name: "missing-target-id", args: args{ targetIdOverride: func() *string { id := ""; return &id }(), createCnt: 5, deleteCnt: 5, }, wantRowsDeleted: 0, wantErr: true, wantErrCode: errors.InvalidParameter, }, { name: "zero-version", args: args{ targetVersionOverride: func() *uint32 { v := uint32(0); return &v }(), createCnt: 5, deleteCnt: 5, }, wantRowsDeleted: 0, wantErr: true, wantErrCode: errors.InvalidParameter, }, { name: "bad-version", args: args{ targetVersionOverride: func() *uint32 { v := uint32(1000); return &v }(), createCnt: 5, deleteCnt: 5, }, wantRowsDeleted: 0, wantErr: true, wantErrCode: errors.VersionMismatch, }, } css := vault.TestCredentialStores(t, conn, wrapper, proj.GetPublicId(), len(tests)) for i, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) cs := css[i] target := TestTcpTarget(t, conn, proj.PublicId, tt.name) clIds := make([]string, 0, tt.args.createCnt) if tt.args.createCnt > 0 { credLibs := vault.TestCredentialLibraries(t, conn, wrapper, cs.PublicId, tt.args.createCnt) for _, cl := range credLibs { clIds = append(clIds, cl.PublicId) } } _, _, addedCredSources, err := repo.AddTargetCredentialSources(context.Background(), target.GetPublicId(), 1, clIds) require.NoError(err) assert.Equal(tt.args.createCnt, len(addedCredSources)) deleteCredSources := make([]string, 0, tt.args.deleteCnt) for i := 0; i < tt.args.deleteCnt; i++ { deleteCredSources = append(deleteCredSources, clIds[i]) } var targetId string switch { case tt.args.targetIdOverride != nil: targetId = *tt.args.targetIdOverride default: targetId = target.GetPublicId() } var targetVersion uint32 switch { case tt.args.targetVersionOverride != nil: targetVersion = *tt.args.targetVersionOverride default: targetVersion = 2 } deletedRows, err := repo.DeleteTargetCredentialSources(context.Background(), targetId, targetVersion, deleteCredSources) if tt.wantErr { require.Error(err) assert.Equal(0, deletedRows) assert.Truef(errors.Match(errors.T(tt.wantErrCode), err), "unexpected error %s", err.Error()) return } require.NoError(err) assert.Equal(tt.wantRowsDeleted, deletedRows) // we should find the oplog for the delete of target credential libraries err = db.TestVerifyOplog(t, rw, target.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second)) assert.NoError(err) }) } t.Run("delete-unassociated", func(t *testing.T) { assert, require := assert.New(t), require.New(t) _, proj := iam.TestScopes(t, iamRepo) cs := vault.TestCredentialStores(t, conn, wrapper, proj.GetPublicId(), 1)[0] libs := vault.TestCredentialLibraries(t, conn, wrapper, cs.GetPublicId(), 3) require.Len(libs, 3) lib1 := libs[0] lib2 := libs[1] lib3 := libs[2] projTarget := TestTcpTarget(t, conn, proj.PublicId, "add-existing") _, _, gotCredSources, err := repo.AddTargetCredentialSources(context.Background(), projTarget.PublicId, 1, []string{lib1.PublicId, lib2.PublicId}) require.NoError(err) assert.Len(gotCredSources, 2) // Deleting an unassociated source should return an error delCount, err := repo.DeleteTargetCredentialSources(context.Background(), projTarget.PublicId, 2, []string{lib3.PublicId}) require.Error(err) assert.True(errors.Match(errors.T(errors.MultipleRecords), err)) assert.Equal(0, delCount) // Deleting sources which includes an unassociated source should return an error delCount, err = repo.DeleteTargetCredentialSources(context.Background(), projTarget.PublicId, 2, []string{lib1.PublicId, lib2.PublicId, lib3.PublicId}) require.Error(err) assert.True(errors.Match(errors.T(errors.MultipleRecords), err)) assert.Equal(0, delCount) // Previous transactions should have been rolled back and only lib1 should be associated gotCredSources, err = fetchCredentialSources(context.Background(), rw, projTarget.PublicId) require.NoError(err) assert.Len(gotCredSources, 2) }) } func TestRepository_SetTargetCredentialSources(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) wrapper := db.TestWrapper(t) testKms := kms.TestKms(t, conn, wrapper) repo, err := NewRepository(rw, rw, testKms) require.NoError(t, err) iamRepo := iam.TestRepo(t, conn, wrapper) _, proj := iam.TestScopes(t, iamRepo) cs := vault.TestCredentialStores(t, conn, wrapper, proj.GetPublicId(), 1)[0] credLibs := vault.TestCredentialLibraries(t, conn, wrapper, cs.GetPublicId(), 2) lib1 := credLibs[0] lib2 := credLibs[1] setupFn := func(target Target) []CredentialSource { credLibs := vault.TestCredentialLibraries(t, conn, wrapper, cs.GetPublicId(), 10) clIds := make([]string, 0, len(credLibs)) for _, cl := range credLibs { clIds = append(clIds, cl.PublicId) } _, _, created, err := repo.AddTargetCredentialSources(context.Background(), target.GetPublicId(), 1, clIds) require.NoError(t, err) require.Equal(t, 10, len(created)) return created } type args struct { targetVersion uint32 clIds []string addToOrigLibs bool } tests := []struct { name string setup func(Target) []CredentialSource args args wantAffectedRows int wantErr bool wantErrCode errors.Code }{ { name: "clear", setup: setupFn, args: args{ targetVersion: 2, clIds: []string{}, }, wantErr: false, wantAffectedRows: 10, }, { name: "no-change", setup: setupFn, args: args{ targetVersion: 2, clIds: []string{}, addToOrigLibs: true, }, wantErr: false, wantAffectedRows: 0, }, { name: "add-cred-sources", setup: setupFn, args: args{ targetVersion: 2, clIds: []string{lib1.PublicId, lib2.PublicId}, addToOrigLibs: true, }, wantErr: false, wantAffectedRows: 2, }, { name: "zero version", setup: setupFn, args: args{ targetVersion: 0, clIds: []string{lib1.PublicId, lib2.PublicId}, addToOrigLibs: true, }, wantErr: true, wantErrCode: errors.InvalidParameter, }, { name: "bad version", setup: setupFn, args: args{ targetVersion: 1000, clIds: []string{lib1.PublicId, lib2.PublicId}, addToOrigLibs: true, }, wantErr: true, wantErrCode: errors.VersionMismatch, }, { name: "remove existing and add cred libs", setup: setupFn, args: args{ targetVersion: 2, clIds: []string{lib1.PublicId, lib2.PublicId}, addToOrigLibs: false, }, wantErr: false, wantAffectedRows: 12, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) target := TestTcpTarget(t, conn, proj.PublicId, tt.name) var origCredSources []CredentialSource if tt.setup != nil { origCredSources = tt.setup(target) } if tt.args.addToOrigLibs { origIds := make([]string, 0, len(origCredSources)) for _, cl := range origCredSources { origIds = append(origIds, cl.Id()) } tt.args.clIds = append(tt.args.clIds, origIds...) } origTarget, _, lookupCredSources, err := repo.LookupTarget(context.Background(), target.GetPublicId()) require.NoError(err) assert.Equal(origCredSources, lookupCredSources) _, got, affectedRows, err := repo.SetTargetCredentialSources(context.Background(), target.GetPublicId(), tt.args.targetVersion, tt.args.clIds) if tt.wantErr { require.Error(err) assert.Equal(0, affectedRows) assert.Truef(errors.Match(errors.T(tt.wantErrCode), err), "unexpected error %s", err.Error()) return } t.Log(err) require.NoError(err) assert.Equal(tt.wantAffectedRows, affectedRows) assert.Equal(len(tt.args.clIds), len(got)) var wantIds []string wantIds = append(wantIds, tt.args.clIds...) sort.Strings(wantIds) var gotIds []string if len(got) > 0 { gotIds = make([]string, 0, len(got)) for _, s := range got { gotIds = append(gotIds, s.Id()) } } sort.Strings(gotIds) assert.Equal(wantIds, gotIds) foundTarget, _, _, err := repo.LookupTarget(context.Background(), target.GetPublicId()) require.NoError(err) if tt.name != "no-change" { assert.Equalf(tt.args.targetVersion+1, foundTarget.GetVersion(), "%s unexpected version: %d/%d", tt.name, tt.args.targetVersion+1, foundTarget.GetVersion()) assert.Equalf(origTarget.GetVersion(), foundTarget.GetVersion()-1, "%s unexpected version: %d/%d", tt.name, origTarget.GetVersion(), foundTarget.GetVersion()-1) } }) } t.Run("missing-target-id", func(t *testing.T) { assert, require := assert.New(t), require.New(t) _, _, _, err := repo.SetTargetCredentialSources(context.Background(), "", 1, []string{lib1.PublicId}) require.Error(err) assert.Truef(errors.Match(errors.T(errors.InvalidParameter), err), "unexpected error %s", err.Error()) }) t.Run("target-not-found", func(t *testing.T) { assert, require := assert.New(t), require.New(t) _, _, _, err := repo.SetTargetCredentialSources(context.Background(), "fake-target-id", 1, []string{lib1.PublicId}) require.Error(err) assert.Truef(errors.Match(errors.T(errors.RecordNotFound), err), "unexpected error %s", err.Error()) }) }