diff --git a/Makefile b/Makefile index 51296c11e..41aef097c 100644 --- a/Makefile +++ b/Makefile @@ -129,6 +129,7 @@ protobuild: @protoc-go-inject-tag -input=./internal/kms/store/session_key.pb.go @protoc-go-inject-tag -input=./internal/kms/store/oidc_key.pb.go @protoc-go-inject-tag -input=./internal/target/store/target.pb.go + @protoc-go-inject-tag -input=./internal/target/targettest/store/target.pb.go @protoc-go-inject-tag -input=./internal/target/tcp/store/target.pb.go @protoc-go-inject-tag -input=./internal/auth/oidc/store/oidc.pb.go @protoc-go-inject-tag -input=./internal/scheduler/job/store/job.pb.go diff --git a/internal/credential/credential.go b/internal/credential/credential.go index 98b311637..5c337ae21 100644 --- a/internal/credential/credential.go +++ b/internal/credential/credential.go @@ -28,6 +28,10 @@ type Library interface { // Purpose is the purpose of the credential. type Purpose string +func (p Purpose) String() string { + return string(p) +} + // Credential purpose values. const ( // ApplicationPurpose is a credential used for application specific @@ -45,6 +49,13 @@ const ( EgressPurpose Purpose = "egress" ) +// ValidPurposes are the set of all credential Purposes. +var ValidPurposes = []Purpose{ + ApplicationPurpose, + IngressPurpose, + EgressPurpose, +} + // SecretData represents secret data. type SecretData interface{} diff --git a/internal/db/schema/migrations/oss/postgres_16_05_test.go b/internal/db/schema/migrations/oss/postgres_16_05_test.go index d2c279751..b63444085 100644 --- a/internal/db/schema/migrations/oss/postgres_16_05_test.go +++ b/internal/db/schema/migrations/oss/postgres_16_05_test.go @@ -165,16 +165,21 @@ func testSessionCredentialParams(t *testing.T, conn *db.DB, kms *kms.Kms, wrappe ctx := context.Background() stores := vault.TestCredentialStores(t, conn, wrapper, tar.GetScopeId(), 1) - libs := vault.TestCredentialLibraries(t, conn, wrapper, stores[0].GetPublicId(), 2) + libIds := vault.TestCredentialLibraries(t, conn, wrapper, stores[0].GetPublicId(), 2) + libs := []*target.CredentialLibrary{ + target.TestNewCredentialLibrary(tar.GetPublicId(), libIds[0].GetPublicId(), credential.ApplicationPurpose), + target.TestNewCredentialLibrary(tar.GetPublicId(), libIds[0].GetPublicId(), credential.IngressPurpose), + target.TestNewCredentialLibrary(tar.GetPublicId(), libIds[1].GetPublicId(), credential.EgressPurpose), + } targetRepo, err := target.NewRepository(rw, rw, kms) require.NoError(t, err) - _, _, _, err = targetRepo.AddTargetCredentialSources(ctx, tar.GetPublicId(), tar.GetVersion(), []string{libs[0].PublicId, libs[1].PublicId}) + _, _, _, err = targetRepo.AddTargetCredentialSources(ctx, tar.GetPublicId(), tar.GetVersion(), libs) require.NoError(t, err) creds := []*session.DynamicCredential{ - session.NewDynamicCredential(libs[0].GetPublicId(), credential.ApplicationPurpose), - session.NewDynamicCredential(libs[0].GetPublicId(), credential.IngressPurpose), - session.NewDynamicCredential(libs[1].GetPublicId(), credential.EgressPurpose), + session.NewDynamicCredential(libIds[0].GetPublicId(), credential.ApplicationPurpose), + session.NewDynamicCredential(libIds[0].GetPublicId(), credential.IngressPurpose), + session.NewDynamicCredential(libIds[1].GetPublicId(), credential.EgressPurpose), } return creds } diff --git a/internal/proto/local/controller/storage/target/targettest/store/v1/target.proto b/internal/proto/local/controller/storage/target/targettest/store/v1/target.proto new file mode 100644 index 000000000..b9471c819 --- /dev/null +++ b/internal/proto/local/controller/storage/target/targettest/store/v1/target.proto @@ -0,0 +1,73 @@ +syntax = "proto3"; + +package controller.storage.target.targettest.store.v1; +option go_package = "github.com/hashicorp/boundary/internal/target/targettest/store;store"; + +import "controller/storage/timestamp/v1/timestamp.proto"; +import "controller/custom_options/v1/options.proto"; + +message Target { + // public_id is used to access the targettest.Target via an API + // @inject_tag: gorm:"primary_key" + string public_id = 10; + + // scope id for the targettest.Target + // @inject_tag: `gorm:"default:null"` + string scope_id = 20; + + // name is the optional friendly name used to + // access the targettest.Target via an API + // @inject_tag: `gorm:"default:null"` + string name = 30 + [(custom_options.v1.mask_mapping) = { this: "name" that: "name" }]; + + // description of the targettest.Target + // @inject_tag: `gorm:"default:null"` + string description = 40 [(custom_options.v1.mask_mapping) = { + this: "description" + that: "description" + }]; + + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 50; + + // update_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp update_time = 60; + + // version allows optimistic locking of the targettest.Target when modifying the + // targettest.Target + // @inject_tag: `gorm:"default:null"` + uint32 version = 70; + + // default port of the targettest.Target + // @inject_tag: `gorm:"default:null"` + uint32 default_port = 80 [(custom_options.v1.mask_mapping) = { + this: "DefaultPort" + that: "attributes.default_port" + }]; + + // Maximum total lifetime of a created session, in seconds + // @inject_tag: `gorm:"default:null"` + uint32 session_max_seconds = 100 [(custom_options.v1.mask_mapping) = { + this: "SessionMaxSeconds" + that: "session_max_seconds" + }]; + + // Maximum number of connections in a session + // @inject_tag: `gorm:"default:null"` + int32 session_connection_limit = 110 [(custom_options.v1.mask_mapping) = { + this: "SessionConnectionLimit" + that: "session_connection_limit" + }]; + + // A boolean expression that allows filtering the workers that can handle a session + // @inject_tag: `gorm:"default:null"` + string worker_filter = 120 [(custom_options.v1.mask_mapping) = { + this: "WorkerFilter" + that: "worker_filter" + }]; +} + + diff --git a/internal/servers/controller/handlers/targets/target_service.go b/internal/servers/controller/handlers/targets/target_service.go index 78c1261de..4c9f9eb1b 100644 --- a/internal/servers/controller/handlers/targets/target_service.go +++ b/internal/servers/controller/handlers/targets/target_service.go @@ -1346,12 +1346,17 @@ func (s Service) removeHostSourcesInRepo(ctx context.Context, targetId string, h return out, hs, cl, nil } -func (s Service) addCredentialSourcesInRepo(ctx context.Context, targetId string, ids []string, version uint32) (target.Target, []target.HostSource, []target.CredentialSource, error) { +func (s Service) addCredentialSourcesInRepo(ctx context.Context, targetId string, applicationIds []string, version uint32) (target.Target, []target.HostSource, []target.CredentialSource, error) { repo, err := s.repoFn() if err != nil { return nil, nil, nil, err } - out, hs, credSources, err := repo.AddTargetCredentialSources(ctx, targetId, version, strutil.RemoveDuplicates(ids, false)) + credLibs, err := createCredLibs(targetId, applicationIds, nil, nil) + if err != nil { + return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to set credential sources in target: %v.", err) + } + + out, hs, credSources, err := repo.AddTargetCredentialSources(ctx, targetId, version, credLibs) if err != nil { // TODO: Figure out a way to surface more helpful error info beyond the Internal error. return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to add credential sources to target: %v.", err) @@ -1362,13 +1367,19 @@ func (s Service) addCredentialSourcesInRepo(ctx context.Context, targetId string return out, hs, credSources, nil } -func (s Service) setCredentialSourcesInRepo(ctx context.Context, targetId string, ids []string, version uint32) (target.Target, []target.HostSource, []target.CredentialSource, error) { +func (s Service) setCredentialSourcesInRepo(ctx context.Context, targetId string, applicationIds []string, version uint32) (target.Target, []target.HostSource, []target.CredentialSource, error) { const op = "targets.(Service).setCredentialSourcesInRepo" repo, err := s.repoFn() if err != nil { return nil, nil, nil, err } - _, _, _, err = repo.SetTargetCredentialSources(ctx, targetId, version, strutil.RemoveDuplicates(ids, false)) + + credLibs, err := createCredLibs(targetId, applicationIds, nil, nil) + if err != nil { + return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to set credential sources in target: %v.", err) + } + + _, _, _, err = repo.SetTargetCredentialSources(ctx, targetId, version, credLibs) if err != nil { // TODO: Figure out a way to surface more helpful error info beyond the Internal error. return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to set credential sources in target: %v.", err) @@ -1384,13 +1395,18 @@ func (s Service) setCredentialSourcesInRepo(ctx context.Context, targetId string return out, hs, credSources, nil } -func (s Service) removeCredentialSourcesInRepo(ctx context.Context, targetId string, ids []string, version uint32) (target.Target, []target.HostSource, []target.CredentialSource, error) { +func (s Service) removeCredentialSourcesInRepo(ctx context.Context, targetId string, applicationIds []string, version uint32) (target.Target, []target.HostSource, []target.CredentialSource, error) { const op = "targets.(Service).removeCredentialSourcesInRepo" repo, err := s.repoFn() if err != nil { return nil, nil, nil, err } - _, err = repo.DeleteTargetCredentialSources(ctx, targetId, version, strutil.RemoveDuplicates(ids, false)) + + credLibs, err := createCredLibs(targetId, applicationIds, nil, nil) + if err != nil { + return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to set credential sources in target: %v.", err) + } + _, err = repo.DeleteTargetCredentialSources(ctx, targetId, version, credLibs) if err != nil { // TODO: Figure out a way to surface more helpful error info beyond the Internal error. return nil, nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to remove credential sources from target: %v.", err) @@ -2003,3 +2019,23 @@ func validateAuthorizeSessionRequest(req *pbs.AuthorizeSessionRequest) error { } return nil } + +func createCredLibs(targetId string, applicationIds, ingressIds, egressIds []string) ([]*target.CredentialLibrary, error) { + credLibs := make([]*target.CredentialLibrary, 0, len(applicationIds)+len(ingressIds)+len(egressIds)) + + byPurpose := map[credential.Purpose][]string{ + credential.ApplicationPurpose: strutil.RemoveDuplicates(applicationIds, false), + credential.IngressPurpose: strutil.RemoveDuplicates(ingressIds, false), + credential.EgressPurpose: strutil.RemoveDuplicates(egressIds, false), + } + for purpose, ids := range byPurpose { + for _, id := range ids { + l, err := target.NewCredentialLibrary(targetId, id, purpose) + if err != nil { + return nil, err + } + credLibs = append(credLibs, l) + } + } + return credLibs, nil +} diff --git a/internal/servers/controller/handlers/targets/target_service_test.go b/internal/servers/controller/handlers/targets/target_service_test.go index 700ad493b..8c77191df 100644 --- a/internal/servers/controller/handlers/targets/target_service_test.go +++ b/internal/servers/controller/handlers/targets/target_service_test.go @@ -14,6 +14,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/credential/vault" "github.com/hashicorp/boundary/internal/db" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" @@ -1704,13 +1705,13 @@ func TestAddTargetLibraries(t *testing.T) { }, { name: "Add library on populated target", - tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "populated for libraries", target.WithCredentialSources([]string{cls[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "populated for libraries", target.WithCredentialLibraries([]*target.CredentialLibrary{target.TestNewCredentialLibrary("", cls[0].GetPublicId(), credential.ApplicationPurpose)})), addLibraries: []string{cls[1].GetPublicId()}, resultLibraryIds: []string{cls[0].GetPublicId(), cls[1].GetPublicId()}, }, { name: "Add duplicated libraries on populated target", - tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "duplicated for libraries", target.WithCredentialSources([]string{cls[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "duplicated for libraries", target.WithCredentialLibraries([]*target.CredentialLibrary{target.TestNewCredentialLibrary("", cls[0].GetPublicId(), credential.ApplicationPurpose)})), addLibraries: []string{cls[1].GetPublicId(), cls[1].GetPublicId()}, resultLibraryIds: []string{cls[0].GetPublicId(), cls[1].GetPublicId()}, }, @@ -1840,19 +1841,19 @@ func TestSetTargetLibraries(t *testing.T) { }, { name: "Set on populated target", - tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "populated", target.WithCredentialSources([]string{cls[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "populated", target.WithCredentialLibraries([]*target.CredentialLibrary{target.TestNewCredentialLibrary("", cls[0].GetPublicId(), credential.ApplicationPurpose)})), setLibraries: []string{cls[1].GetPublicId()}, resultLibraryIds: []string{cls[1].GetPublicId()}, }, { name: "Set duplicate libraries on populated target", - tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "duplicate", target.WithCredentialSources([]string{cls[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "duplicate", target.WithCredentialLibraries([]*target.CredentialLibrary{target.TestNewCredentialLibrary("", cls[0].GetPublicId(), credential.ApplicationPurpose)})), setLibraries: []string{cls[1].GetPublicId(), cls[1].GetPublicId()}, resultLibraryIds: []string{cls[1].GetPublicId()}, }, { name: "Set empty on populated target", - tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "another populated", target.WithCredentialSources([]string{cls[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "another populated", target.WithCredentialLibraries([]*target.CredentialLibrary{target.TestNewCredentialLibrary("", cls[0].GetPublicId(), credential.ApplicationPurpose)})), setLibraries: []string{}, resultLibraryIds: nil, }, @@ -1966,13 +1967,13 @@ func TestRemoveTargetLibraries(t *testing.T) { }, { name: "Remove 1 of 2 libraries", - tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "remove partial", target.WithCredentialSources([]string{cls[0].GetPublicId(), cls[1].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "remove partial", target.WithCredentialLibraries([]*target.CredentialLibrary{target.TestNewCredentialLibrary("", cls[0].GetPublicId(), credential.ApplicationPurpose), target.TestNewCredentialLibrary("", cls[1].GetPublicId(), credential.ApplicationPurpose)})), removeLibs: []string{cls[1].GetPublicId()}, resultLibIds: []string{cls[0].GetPublicId()}, }, { name: "Remove 1 duplicate set of 2 libraries", - tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "remove duplicate", target.WithCredentialSources([]string{cls[0].GetPublicId(), cls[1].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "remove duplicate", target.WithCredentialLibraries([]*target.CredentialLibrary{target.TestNewCredentialLibrary("", cls[0].GetPublicId(), credential.ApplicationPurpose), target.TestNewCredentialLibrary("", cls[1].GetPublicId(), credential.ApplicationPurpose)})), removeLibs: []string{ cls[1].GetPublicId(), cls[1].GetPublicId(), }, @@ -1980,7 +1981,7 @@ func TestRemoveTargetLibraries(t *testing.T) { }, { name: "Remove all libraries from target", - tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "remove all", target.WithCredentialSources([]string{cls[0].GetPublicId(), cls[1].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "remove all", target.WithCredentialLibraries([]*target.CredentialLibrary{target.TestNewCredentialLibrary("", cls[0].GetPublicId(), credential.ApplicationPurpose), target.TestNewCredentialLibrary("", cls[1].GetPublicId(), credential.ApplicationPurpose)})), removeLibs: []string{ cls[0].GetPublicId(), cls[1].GetPublicId(), }, @@ -2101,13 +2102,13 @@ func TestAddTargetSources(t *testing.T) { }, { name: "Add source on populated target", - tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "populated for sources", target.WithCredentialSources([]string{cls[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "populated for sources", target.WithCredentialLibraries([]*target.CredentialLibrary{target.TestNewCredentialLibrary("", cls[0].GetPublicId(), credential.ApplicationPurpose)})), addSources: []string{cls[1].GetPublicId()}, resultSourceIds: []string{cls[0].GetPublicId(), cls[1].GetPublicId()}, }, { name: "Add duplicated sources on populated target", - tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "duplicated for sources", target.WithCredentialSources([]string{cls[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "duplicated for sources", target.WithCredentialLibraries([]*target.CredentialLibrary{target.TestNewCredentialLibrary("", cls[0].GetPublicId(), credential.ApplicationPurpose)})), addSources: []string{cls[1].GetPublicId(), cls[1].GetPublicId()}, resultSourceIds: []string{cls[0].GetPublicId(), cls[1].GetPublicId()}, }, @@ -2237,19 +2238,19 @@ func TestSetTargetCredentialSources(t *testing.T) { }, { name: "Set on populated target", - tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "populated", target.WithCredentialSources([]string{cls[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "populated", target.WithCredentialLibraries([]*target.CredentialLibrary{target.TestNewCredentialLibrary("", cls[0].GetPublicId(), credential.ApplicationPurpose)})), setCredentialSources: []string{cls[1].GetPublicId()}, resultCredentialSourceIds: []string{cls[1].GetPublicId()}, }, { name: "Set duplicate sources on populated target", - tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "duplicate", target.WithCredentialSources([]string{cls[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "duplicate", target.WithCredentialLibraries([]*target.CredentialLibrary{target.TestNewCredentialLibrary("", cls[0].GetPublicId(), credential.ApplicationPurpose)})), setCredentialSources: []string{cls[1].GetPublicId(), cls[1].GetPublicId()}, resultCredentialSourceIds: []string{cls[1].GetPublicId()}, }, { name: "Set empty on populated target", - tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "another populated", target.WithCredentialSources([]string{cls[0].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "another populated", target.WithCredentialLibraries([]*target.CredentialLibrary{target.TestNewCredentialLibrary("", cls[0].GetPublicId(), credential.ApplicationPurpose)})), setCredentialSources: []string{}, resultCredentialSourceIds: nil, }, @@ -2363,13 +2364,13 @@ func TestRemoveTargetCredentialSources(t *testing.T) { }, { name: "Remove 1 of 2 sources", - tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "remove partial", target.WithCredentialSources([]string{cls[0].GetPublicId(), cls[1].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "remove partial", target.WithCredentialLibraries([]*target.CredentialLibrary{target.TestNewCredentialLibrary("", cls[0].GetPublicId(), credential.ApplicationPurpose), target.TestNewCredentialLibrary("", cls[1].GetPublicId(), credential.ApplicationPurpose)})), removeCredentialSources: []string{cls[1].GetPublicId()}, resultCredentialSourceIds: []string{cls[0].GetPublicId()}, }, { name: "Remove 1 duplicate set of 2 sources", - tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "remove duplicate", target.WithCredentialSources([]string{cls[0].GetPublicId(), cls[1].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "remove duplicate", target.WithCredentialLibraries([]*target.CredentialLibrary{target.TestNewCredentialLibrary("", cls[0].GetPublicId(), credential.ApplicationPurpose), target.TestNewCredentialLibrary("", cls[1].GetPublicId(), credential.ApplicationPurpose)})), removeCredentialSources: []string{ cls[1].GetPublicId(), cls[1].GetPublicId(), }, @@ -2377,7 +2378,7 @@ func TestRemoveTargetCredentialSources(t *testing.T) { }, { name: "Remove all sources from target", - tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "remove all", target.WithCredentialSources([]string{cls[0].GetPublicId(), cls[1].GetPublicId()})), + tar: tcp.TestTarget(t, conn, proj.GetPublicId(), "remove all", target.WithCredentialLibraries([]*target.CredentialLibrary{target.TestNewCredentialLibrary("", cls[0].GetPublicId(), credential.ApplicationPurpose), target.TestNewCredentialLibrary("", cls[1].GetPublicId(), credential.ApplicationPurpose)})), removeCredentialSources: []string{ cls[0].GetPublicId(), cls[1].GetPublicId(), }, diff --git a/internal/session/repository_session_test.go b/internal/session/repository_session_test.go index 68e9076f1..f2107a71d 100644 --- a/internal/session/repository_session_test.go +++ b/internal/session/repository_session_test.go @@ -1649,21 +1649,28 @@ func testSessionCredentialParams(t *testing.T, conn *db.DB, wrapper wrapping.Wra rw := db.New(conn) ctx := context.Background() - stores := vault.TestCredentialStores(t, conn, wrapper, params.ScopeId, 1) - libs := vault.TestCredentialLibraries(t, conn, wrapper, stores[0].GetPublicId(), 2) kms := kms.TestKms(t, conn, wrapper) targetRepo, err := target.NewRepository(rw, rw, kms) require.NoError(err) - tcpTarget, _, _, err := targetRepo.LookupTarget(ctx, params.TargetId) + tar, _, _, err := targetRepo.LookupTarget(ctx, params.TargetId) require.NoError(err) - require.NotNil(tcpTarget) - _, _, _, err = targetRepo.AddTargetCredentialSources(ctx, tcpTarget.GetPublicId(), tcpTarget.GetVersion(), []string{libs[0].PublicId, libs[1].PublicId}) + require.NotNil(tar) + + stores := vault.TestCredentialStores(t, conn, wrapper, params.ScopeId, 1) + libIds := vault.TestCredentialLibraries(t, conn, wrapper, stores[0].GetPublicId(), 2) + libs := []*target.CredentialLibrary{ + target.TestNewCredentialLibrary(tar.GetPublicId(), libIds[0].GetPublicId(), credential.ApplicationPurpose), + target.TestNewCredentialLibrary(tar.GetPublicId(), libIds[0].GetPublicId(), credential.IngressPurpose), + target.TestNewCredentialLibrary(tar.GetPublicId(), libIds[1].GetPublicId(), credential.EgressPurpose), + } + + _, _, _, err = targetRepo.AddTargetCredentialSources(ctx, tar.GetPublicId(), tar.GetVersion(), libs) require.NoError(err) creds := []*DynamicCredential{ - NewDynamicCredential(libs[0].GetPublicId(), credential.ApplicationPurpose), - NewDynamicCredential(libs[0].GetPublicId(), credential.IngressPurpose), - NewDynamicCredential(libs[1].GetPublicId(), credential.EgressPurpose), + NewDynamicCredential(libIds[0].GetPublicId(), credential.ApplicationPurpose), + NewDynamicCredential(libIds[0].GetPublicId(), credential.IngressPurpose), + NewDynamicCredential(libIds[1].GetPublicId(), credential.EgressPurpose), } params.DynamicCredentials = creds return params diff --git a/internal/target/credential_library.go b/internal/target/credential_library.go index 97d83f811..8c013a4d8 100644 --- a/internal/target/credential_library.go +++ b/internal/target/credential_library.go @@ -21,7 +21,7 @@ type CredentialLibrary struct { // NewCredentialLibrary creates a new in memory CredentialLibrary // representing the relationship between targetId and credentialLibraryId. -func NewCredentialLibrary(targetId, credentialLibraryId string, _ ...Option) (*CredentialLibrary, error) { +func NewCredentialLibrary(targetId, credentialLibraryId string, purpose credential.Purpose) (*CredentialLibrary, error) { const op = "target.NewCredentialLibrary" if targetId == "" { return nil, errors.NewDeprecated(errors.InvalidParameter, op, "no target id") @@ -34,7 +34,7 @@ func NewCredentialLibrary(targetId, credentialLibraryId string, _ ...Option) (*C CredentialLibrary: &store.CredentialLibrary{ TargetId: targetId, CredentialLibraryId: credentialLibraryId, - CredentialPurpose: string(credential.ApplicationPurpose), // application is the only purpose currently supported + CredentialPurpose: string(purpose), }, } return t, nil diff --git a/internal/target/credential_library_test.go b/internal/target/credential_library_test.go index a079f471b..fb36b27f9 100644 --- a/internal/target/credential_library_test.go +++ b/internal/target/credential_library_test.go @@ -55,7 +55,7 @@ func TestCredentialLibrary_New(t *testing.T) { tt := tt t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - got, err := target.NewCredentialLibrary(tt.args.targetId, tt.args.libraryId) + got, err := target.NewCredentialLibrary(tt.args.targetId, tt.args.libraryId, credential.ApplicationPurpose) if tt.wantErr != 0 { assert.Truef(errors.Match(errors.T(tt.wantErr), err), "want err: %q got: %q", tt.wantErr, err) assert.Nil(got) diff --git a/internal/target/options.go b/internal/target/options.go index 5fa1bbe08..69565cd3c 100644 --- a/internal/target/options.go +++ b/internal/target/options.go @@ -30,7 +30,7 @@ type options struct { WithUserId string WithType subtypes.Subtype WithHostSources []string - WithCredentialSources []string + WithCredentialLibraries []*CredentialLibrary WithSessionMaxSeconds uint32 WithSessionConnectionLimit int32 WithPublicId string @@ -49,7 +49,7 @@ func getDefaultOptions() options { WithUserId: "", WithType: "", WithHostSources: nil, - WithCredentialSources: nil, + WithCredentialLibraries: nil, WithSessionMaxSeconds: uint32((8 * time.Hour).Seconds()), WithSessionConnectionLimit: 1, WithPublicId: "", @@ -129,10 +129,10 @@ func WithHostSources(hs []string) Option { } } -// WithCredentialSources provides an option for providing a list of credential source ids -func WithCredentialSources(cl []string) Option { +// WithCredentialLibraries provides an option for providing a list of credential libraries. +func WithCredentialLibraries(cl []*CredentialLibrary) Option { return func(o *options) { - o.WithCredentialSources = cl + o.WithCredentialLibraries = cl } } diff --git a/internal/target/options_test.go b/internal/target/options_test.go index a4d3431f5..0746102d6 100644 --- a/internal/target/options_test.go +++ b/internal/target/options_test.go @@ -3,6 +3,8 @@ package target import ( "testing" + "github.com/hashicorp/boundary/internal/credential" + "github.com/hashicorp/boundary/internal/target/store" "github.com/hashicorp/boundary/internal/types/subtypes" "github.com/stretchr/testify/assert" ) @@ -106,11 +108,37 @@ func Test_GetOpts(t *testing.T) { testOpts.WithWorkerFilter = `"/foo" == "bar"` assert.Equal(opts, testOpts) }) - t.Run("WithCredentialSources", func(t *testing.T) { + t.Run("WithCredentialLibraries", func(t *testing.T) { assert := assert.New(t) - opts := GetOpts(WithCredentialSources([]string{"alice", "bob"})) + opts := GetOpts(WithCredentialLibraries([]*CredentialLibrary{ + { + CredentialLibrary: &store.CredentialLibrary{ + CredentialLibraryId: "alice", + CredentialPurpose: string(credential.ApplicationPurpose), + }, + }, + { + CredentialLibrary: &store.CredentialLibrary{ + CredentialLibraryId: "bob", + CredentialPurpose: string(credential.EgressPurpose), + }, + }, + })) testOpts := getDefaultOptions() - testOpts.WithCredentialSources = []string{"alice", "bob"} + testOpts.WithCredentialLibraries = []*CredentialLibrary{ + { + CredentialLibrary: &store.CredentialLibrary{ + CredentialLibraryId: "alice", + CredentialPurpose: string(credential.ApplicationPurpose), + }, + }, + { + CredentialLibrary: &store.CredentialLibrary{ + CredentialLibraryId: "bob", + CredentialPurpose: string(credential.EgressPurpose), + }, + }, + } assert.Equal(opts, testOpts) }) } diff --git a/internal/target/query.go b/internal/target/query.go index 7dc5c09a4..054861eee 100644 --- a/internal/target/query.go +++ b/internal/target/query.go @@ -13,7 +13,8 @@ current_libraries (library_id) as ( -- returns the current list select credential_library_id from target_credential_library - where target_id = @target_id + where target_id = @target_id + and credential_purpose = @purpose ), keep_libraries (library_id) as ( -- returns the KEEP list diff --git a/internal/target/repository_credential_source.go b/internal/target/repository_credential_source.go index 31284bf1e..2cad2a345 100644 --- a/internal/target/repository_credential_source.go +++ b/internal/target/repository_credential_source.go @@ -6,17 +6,46 @@ import ( "fmt" "strings" + "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/oplog" + "github.com/hashicorp/boundary/internal/target/store" ) +// credentialLibrariesPurposeMap is used to group CredentialLibraries by purpose. +type credentialLibrariesPurposeMap map[string][]*CredentialLibrary + +func (c credentialLibrariesPurposeMap) add(cl *CredentialLibrary) { + var s []*CredentialLibrary + var ok bool + + s, ok = c[cl.CredentialPurpose] + if !ok { + s = make([]*CredentialLibrary, 0, 1) + } + + s = append(s, cl) + c[cl.CredentialPurpose] = s +} + +func newCredentialLibrariesPurposeMap(cls []*CredentialLibrary) credentialLibrariesPurposeMap { + clpm := make(credentialLibrariesPurposeMap) + for _, p := range credential.ValidPurposes { + clpm[p.String()] = make([]*CredentialLibrary, 0) + } + for _, cl := range cls { + clpm.add(cl) + } + return clpm +} + // AddTargetCredentialSources adds the cIds to the targetId in the repository. The target // and the list of credential sources attached to the target, after cIds are added, // will be returned on success. // The targetVersion must match the current version of the targetId in the repository. -func (r *Repository) AddTargetCredentialSources(ctx context.Context, targetId string, targetVersion uint32, cIds []string, _ ...Option) (Target, []HostSource, []CredentialSource, error) { +func (r *Repository) AddTargetCredentialSources(ctx context.Context, targetId string, targetVersion uint32, cls []*CredentialLibrary, _ ...Option) (Target, []HostSource, []CredentialSource, error) { const op = "target.(Repository).AddTargetCredentialSources" if targetId == "" { return nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing target id") @@ -24,16 +53,12 @@ func (r *Repository) AddTargetCredentialSources(ctx context.Context, targetId st if targetVersion == 0 { return nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing version") } - if len(cIds) == 0 { - return nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing credential source ids") + if len(cls) == 0 { + return nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing credential libraries") } - addCredLibs := make([]interface{}, 0, len(cIds)) - for _, id := range cIds { - cl, err := NewCredentialLibrary(targetId, id) - if err != nil { - return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory credential library")) - } + addCredLibs := make([]interface{}, 0, len(cls)) + for _, cl := range cls { addCredLibs = append(addCredLibs, cl) } @@ -115,7 +140,7 @@ func (r *Repository) AddTargetCredentialSources(ctx context.Context, targetId st // DeleteTargetCredentialSources deletes credential sources from a target in the repository. // The target's current db version must match the targetVersion or an error will be returned. -func (r *Repository) DeleteTargetCredentialSources(ctx context.Context, targetId string, targetVersion uint32, csIds []string, _ ...Option) (int, error) { +func (r *Repository) DeleteTargetCredentialSources(ctx context.Context, targetId string, targetVersion uint32, cls []*CredentialLibrary, _ ...Option) (int, error) { const op = "target.(Repository).DeleteTargetCredentialSources" if targetId == "" { return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing target id") @@ -123,16 +148,12 @@ func (r *Repository) DeleteTargetCredentialSources(ctx context.Context, targetId if targetVersion == 0 { return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version") } - if len(csIds) == 0 { + if len(cls) == 0 { return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing credential source ids") } - deleteCredLibs := make([]interface{}, 0, len(csIds)) - for _, id := range csIds { - cl, err := NewCredentialLibrary(targetId, id) - if err != nil { - return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory credential library")) - } + deleteCredLibs := make([]interface{}, 0, len(cls)) + for _, cl := range cls { deleteCredLibs = append(deleteCredLibs, cl) } @@ -208,7 +229,7 @@ func (r *Repository) DeleteTargetCredentialSources(ctx context.Context, targetId // SetTargetCredentialSources will set the target's credential sources. Set will add // and/or delete credential sources as need to reconcile the existing credential sources // with the request. If clIds is empty, all the credential sources will be cleared from the target. -func (r *Repository) SetTargetCredentialSources(ctx context.Context, targetId string, targetVersion uint32, csIds []string, _ ...Option) ([]HostSource, []CredentialSource, int, error) { +func (r *Repository) SetTargetCredentialSources(ctx context.Context, targetId string, targetVersion uint32, cls []*CredentialLibrary, _ ...Option) ([]HostSource, []CredentialSource, int, error) { const op = "target.(Repository).SetTargetCredentialSources" if targetId == "" { return nil, nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing target id") @@ -217,10 +238,18 @@ func (r *Repository) SetTargetCredentialSources(ctx context.Context, targetId st return nil, nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version") } - changes, err := r.changes(ctx, targetId, csIds) - if err != nil { - return nil, nil, db.NoRowsAffected, errors.Wrap(ctx, err, op) + clpm := newCredentialLibrariesPurposeMap(cls) + + var changes []*change + + for purpose, cls := range clpm { + c, err := r.changes(ctx, targetId, cls, purpose) + if err != nil { + return nil, nil, db.NoRowsAffected, errors.Wrap(ctx, err, op) + } + changes = append(changes, c...) } + if len(changes) == 0 { // Nothing needs to be changed, return early hostSets, err := fetchHostSources(ctx, r.reader, targetId) @@ -236,15 +265,11 @@ func (r *Repository) SetTargetCredentialSources(ctx context.Context, targetId st var deleteCredLibs, addCredLibs []interface{} for _, c := range changes { - cl, err := NewCredentialLibrary(targetId, c.LibraryId) - if err != nil { - return nil, nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory target credential library")) - } - switch c.Action { + switch c.action { case "delete": - deleteCredLibs = append(deleteCredLibs, cl) + deleteCredLibs = append(deleteCredLibs, c.credentialLibrary) case "add": - addCredLibs = append(addCredLibs, cl) + addCredLibs = append(addCredLibs, c.credentialLibrary) } } @@ -343,28 +368,33 @@ func (r *Repository) SetTargetCredentialSources(ctx context.Context, targetId st } type change struct { + action string + credentialLibrary *CredentialLibrary +} + +type changeQueryResult struct { Action string LibraryId string } -func (r *Repository) changes(ctx context.Context, targetId string, clIds []string) ([]*change, error) { +func (r *Repository) changes(ctx context.Context, targetId string, cls []*CredentialLibrary, purpose string) ([]*change, error) { const op = "target.(Repository).changes" + + // TODO ensure that all cls have the same purpose as the given purpose? + var inClauseSpots []string - // starts at 2 because there is already a @1 in the query - for i := 2; i < len(clIds)+2; i++ { - inClauseSpots = append(inClauseSpots, fmt.Sprintf("@%d", i)) + var params []interface{} + params = append(params, sql.Named("target_id", targetId), sql.Named("purpose", purpose)) + for idx, cl := range cls { + params = append(params, sql.Named(fmt.Sprintf("%d", idx+1), cl.CredentialLibraryId)) + inClauseSpots = append(inClauseSpots, fmt.Sprintf("@%d", idx+1)) } inClause := strings.Join(inClauseSpots, ",") if inClause == "" { inClause = "''" } - query := fmt.Sprintf(setChangesQuery, inClause) - var params []interface{} - params = append(params, sql.Named("target_id", targetId)) - for idx, id := range clIds { - params = append(params, sql.Named(fmt.Sprintf("%d", idx+2), id)) - } + query := fmt.Sprintf(setChangesQuery, inClause) rows, err := r.reader.Query(ctx, query, params) if err != nil { return nil, errors.Wrap(ctx, err, op, errors.WithMsg("query failed")) @@ -373,11 +403,20 @@ func (r *Repository) changes(ctx context.Context, targetId string, clIds []strin var changes []*change for rows.Next() { - var chg change + var chg changeQueryResult if err := r.reader.ScanRows(rows, &chg); err != nil { return nil, errors.Wrap(ctx, err, op, errors.WithMsg("scan row failed")) } - changes = append(changes, &chg) + changes = append(changes, &change{ + action: chg.Action, + credentialLibrary: &CredentialLibrary{ + CredentialLibrary: &store.CredentialLibrary{ + TargetId: targetId, + CredentialLibraryId: chg.LibraryId, + CredentialPurpose: purpose, + }, + }, + }) } return changes, nil } diff --git a/internal/target/repository_credential_source_test.go b/internal/target/repository_credential_source_test.go new file mode 100644 index 000000000..46cf8985c --- /dev/null +++ b/internal/target/repository_credential_source_test.go @@ -0,0 +1,254 @@ +package target_test + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/credential" + "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/target" + "github.com/hashicorp/boundary/internal/target/targettest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRepository_SetTargetCredentialSources(t *testing.T) { + target.Register(targettest.Subtype, targettest.Alloc, targettest.Vet, targettest.TargetPrefix) + + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, conn, wrapper) + repo, err := target.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(tar target.Target) ([]target.CredentialSource, []*target.CredentialLibrary) { + credLibs := vault.TestCredentialLibraries(t, conn, wrapper, cs.GetPublicId(), 10) + cls := make([]*target.CredentialLibrary, 0, len(credLibs)) + for _, cl := range credLibs { + cls = append(cls, target.TestNewCredentialLibrary(tar.GetPublicId(), cl.PublicId, credential.ApplicationPurpose)) + } + + _, _, created, err := repo.AddTargetCredentialSources(context.Background(), tar.GetPublicId(), 1, cls) + require.NoError(t, err) + require.Equal(t, 10, len(created)) + return created, cls + } + type args struct { + targetVersion uint32 + cls []*target.CredentialLibrary + addToOrigLibs bool + } + tests := []struct { + name string + setup func(target.Target) ([]target.CredentialSource, []*target.CredentialLibrary) + args args + wantAffectedRows int + wantErr bool + wantErrCode errors.Code + }{ + { + name: "clear", + setup: setupFn, + args: args{ + targetVersion: 2, + cls: []*target.CredentialLibrary{}, + }, + wantErr: false, + wantAffectedRows: 10, + }, + { + name: "no-change", + setup: setupFn, + args: args{ + targetVersion: 2, + cls: []*target.CredentialLibrary{}, + addToOrigLibs: true, + }, + wantErr: false, + wantAffectedRows: 0, + }, + { + name: "add-cred-sources", + setup: setupFn, + args: args{ + targetVersion: 2, + cls: []*target.CredentialLibrary{ + target.TestNewCredentialLibrary("", lib1.PublicId, credential.ApplicationPurpose), + target.TestNewCredentialLibrary("", lib2.PublicId, credential.ApplicationPurpose), + }, + addToOrigLibs: true, + }, + wantErr: false, + wantAffectedRows: 2, + }, + { + name: "add-multiple-purposes", + setup: setupFn, + args: args{ + targetVersion: 2, + cls: []*target.CredentialLibrary{ + target.TestNewCredentialLibrary("", lib1.PublicId, credential.ApplicationPurpose), + target.TestNewCredentialLibrary("", lib1.PublicId, credential.EgressPurpose), + target.TestNewCredentialLibrary("", lib2.PublicId, credential.ApplicationPurpose), + }, + addToOrigLibs: true, + }, + wantErr: false, + wantAffectedRows: 3, + }, + { + name: "remove-add-change-purpose", + setup: func(tar target.Target) ([]target.CredentialSource, []*target.CredentialLibrary) { + cls := []*target.CredentialLibrary{ + target.TestNewCredentialLibrary(tar.GetPublicId(), lib1.PublicId, credential.ApplicationPurpose), + target.TestNewCredentialLibrary(tar.GetPublicId(), lib2.PublicId, credential.ApplicationPurpose), + } + + _, _, created, err := repo.AddTargetCredentialSources(context.Background(), tar.GetPublicId(), 1, cls) + require.NoError(t, err) + require.Equal(t, 2, len(created)) + return created, cls + }, + args: args{ + targetVersion: 2, + cls: []*target.CredentialLibrary{ + target.TestNewCredentialLibrary("", lib1.PublicId, credential.EgressPurpose), + target.TestNewCredentialLibrary("", lib2.PublicId, credential.EgressPurpose), + }, + addToOrigLibs: false, + }, + wantErr: false, + wantAffectedRows: 4, + }, + { + name: "zero version", + setup: setupFn, + args: args{ + targetVersion: 0, + cls: []*target.CredentialLibrary{ + target.TestNewCredentialLibrary("", lib1.PublicId, credential.ApplicationPurpose), + target.TestNewCredentialLibrary("", lib2.PublicId, credential.ApplicationPurpose), + }, + addToOrigLibs: true, + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "bad version", + setup: setupFn, + args: args{ + targetVersion: 1000, + cls: []*target.CredentialLibrary{ + target.TestNewCredentialLibrary("", lib1.PublicId, credential.ApplicationPurpose), + target.TestNewCredentialLibrary("", lib2.PublicId, credential.ApplicationPurpose), + }, + addToOrigLibs: true, + }, + wantErr: true, + wantErrCode: errors.VersionMismatch, + }, + { + name: "remove existing and add cred libs", + setup: setupFn, + args: args{ + targetVersion: 2, + cls: []*target.CredentialLibrary{ + target.TestNewCredentialLibrary("", lib1.PublicId, credential.ApplicationPurpose), + target.TestNewCredentialLibrary("", lib2.PublicId, credential.ApplicationPurpose), + }, + 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) + + tar := targettest.TestNewTestTarget(t, conn, proj.PublicId, tt.name) + + var origCredSources []target.CredentialSource + var origCredLibraries []*target.CredentialLibrary + if tt.setup != nil { + origCredSources, origCredLibraries = tt.setup(tar) + } + + wantCredSources := make(map[string]*target.CredentialLibrary) + for _, cl := range tt.args.cls { + cl.TargetId = tar.GetPublicId() + wantCredSources[cl.CredentialLibraryId+"_"+cl.CredentialPurpose] = cl + } + if tt.args.addToOrigLibs { + tt.args.cls = append(tt.args.cls, origCredLibraries...) + for _, cl := range origCredLibraries { + wantCredSources[cl.CredentialLibraryId+"_"+cl.CredentialPurpose] = cl + } + } + + origTarget, _, lookupCredSources, err := repo.LookupTarget(context.Background(), tar.GetPublicId()) + require.NoError(err) + assert.Equal(origCredSources, lookupCredSources) + + _, got, affectedRows, err := repo.SetTargetCredentialSources(context.Background(), tar.GetPublicId(), tt.args.targetVersion, tt.args.cls) + 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(wantCredSources), len(got)) + + for _, cs := range got { + w, ok := wantCredSources[cs.Id()+"_"+string(cs.CredentialPurpose())] + assert.True(ok, "got unexpected credentialsource %v", cs) + assert.Equal(w.CredentialLibraryId, cs.Id()) + assert.Equal(w.CredentialPurpose, string(cs.CredentialPurpose())) + } + + foundTarget, _, _, err := repo.LookupTarget(context.Background(), tar.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) + + cl1 := target.TestNewCredentialLibrary("", lib1.PublicId, credential.ApplicationPurpose) + _, _, _, err := repo.SetTargetCredentialSources(context.Background(), "", 1, []*target.CredentialLibrary{cl1}) + + 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) + + cl1 := target.TestNewCredentialLibrary("fake-target-id", lib1.PublicId, credential.ApplicationPurpose) + _, _, _, err := repo.SetTargetCredentialSources(context.Background(), "fake-target-id", 1, []*target.CredentialLibrary{cl1}) + + require.Error(err) + assert.Truef(errors.Match(errors.T(errors.RecordNotFound), err), "unexpected error %s", err.Error()) + }) +} diff --git a/internal/target/targettest/store/target.pb.go b/internal/target/targettest/store/target.pb.go new file mode 100644 index 000000000..cb4e299fc --- /dev/null +++ b/internal/target/targettest/store/target.pb.go @@ -0,0 +1,307 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.17.3 +// source: controller/storage/target/targettest/store/v1/target.proto + +package store + +import ( + timestamp "github.com/hashicorp/boundary/internal/db/timestamp" + _ "github.com/hashicorp/boundary/sdk/pbs/controller/protooptions" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Target struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // public_id is used to access the targettest.Target via an API + // @inject_tag: gorm:"primary_key" + PublicId string `protobuf:"bytes,10,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" gorm:"primary_key"` + // scope id for the targettest.Target + // @inject_tag: `gorm:"default:null"` + ScopeId string `protobuf:"bytes,20,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"default:null"` + // name is the optional friendly name used to + // access the targettest.Target via an API + // @inject_tag: `gorm:"default:null"` + Name string `protobuf:"bytes,30,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` + // description of the targettest.Target + // @inject_tag: `gorm:"default:null"` + Description string `protobuf:"bytes,40,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"` + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,50,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // update_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + UpdateTime *timestamp.Timestamp `protobuf:"bytes,60,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"` + // version allows optimistic locking of the targettest.Target when modifying the + // targettest.Target + // @inject_tag: `gorm:"default:null"` + Version uint32 `protobuf:"varint,70,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"` + // default port of the targettest.Target + // @inject_tag: `gorm:"default:null"` + DefaultPort uint32 `protobuf:"varint,80,opt,name=default_port,json=defaultPort,proto3" json:"default_port,omitempty" gorm:"default:null"` + // Maximum total lifetime of a created session, in seconds + // @inject_tag: `gorm:"default:null"` + SessionMaxSeconds uint32 `protobuf:"varint,100,opt,name=session_max_seconds,json=sessionMaxSeconds,proto3" json:"session_max_seconds,omitempty" gorm:"default:null"` + // Maximum number of connections in a session + // @inject_tag: `gorm:"default:null"` + SessionConnectionLimit int32 `protobuf:"varint,110,opt,name=session_connection_limit,json=sessionConnectionLimit,proto3" json:"session_connection_limit,omitempty" gorm:"default:null"` + // A boolean expression that allows filtering the workers that can handle a session + // @inject_tag: `gorm:"default:null"` + WorkerFilter string `protobuf:"bytes,120,opt,name=worker_filter,json=workerFilter,proto3" json:"worker_filter,omitempty" gorm:"default:null"` +} + +func (x *Target) Reset() { + *x = Target{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_target_targettest_store_v1_target_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Target) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Target) ProtoMessage() {} + +func (x *Target) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_target_targettest_store_v1_target_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Target.ProtoReflect.Descriptor instead. +func (*Target) Descriptor() ([]byte, []int) { + return file_controller_storage_target_targettest_store_v1_target_proto_rawDescGZIP(), []int{0} +} + +func (x *Target) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *Target) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +func (x *Target) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Target) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Target) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *Target) GetUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *Target) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *Target) GetDefaultPort() uint32 { + if x != nil { + return x.DefaultPort + } + return 0 +} + +func (x *Target) GetSessionMaxSeconds() uint32 { + if x != nil { + return x.SessionMaxSeconds + } + return 0 +} + +func (x *Target) GetSessionConnectionLimit() int32 { + if x != nil { + return x.SessionConnectionLimit + } + return 0 +} + +func (x *Target) GetWorkerFilter() string { + if x != nil { + return x.WorkerFilter + } + return "" +} + +var File_controller_storage_target_targettest_store_v1_target_proto protoreflect.FileDescriptor + +var file_controller_storage_target_targettest_store_v1_target_proto_rawDesc = []byte{ + 0x0a, 0x3a, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x2f, 0x74, 0x61, 0x72, 0x67, + 0x65, 0x74, 0x74, 0x65, 0x73, 0x74, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x31, 0x2f, + 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x2d, 0x63, 0x6f, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, + 0x2e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x2e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x74, 0x65, + 0x73, 0x74, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x2f, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, + 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2a, 0x63, 0x6f, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, + 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xc3, 0x05, 0x0a, 0x06, 0x54, 0x61, 0x72, + 0x67, 0x65, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, + 0x12, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x42, 0x10, 0xc2, 0xdd, 0x29, 0x0c, 0x0a, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1e, 0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x0b, 0x64, 0x65, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, + 0x6d, 0x65, 0x18, 0x32, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, + 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, + 0x3c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, + 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, + 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x46, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x4d, 0x0a, 0x0c, 0x64, 0x65, 0x66, 0x61, 0x75, + 0x6c, 0x74, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x50, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x2a, 0xc2, + 0xdd, 0x29, 0x26, 0x0a, 0x0b, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x50, 0x6f, 0x72, 0x74, + 0x12, 0x17, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x64, 0x65, 0x66, + 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x0b, 0x64, 0x65, 0x66, 0x61, 0x75, + 0x6c, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x5c, 0x0a, 0x13, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x64, 0x20, + 0x01, 0x28, 0x0d, 0x42, 0x2c, 0xc2, 0xdd, 0x29, 0x28, 0x0a, 0x11, 0x53, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x4d, 0x61, 0x78, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x13, 0x73, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, + 0x73, 0x52, 0x11, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x78, 0x53, 0x65, 0x63, + 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x70, 0x0a, 0x18, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, + 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, + 0x18, 0x6e, 0x20, 0x01, 0x28, 0x05, 0x42, 0x36, 0xc2, 0xdd, 0x29, 0x32, 0x0a, 0x16, 0x53, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4c, + 0x69, 0x6d, 0x69, 0x74, 0x12, 0x18, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x52, 0x16, + 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x46, 0x0a, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, + 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x78, 0x20, 0x01, 0x28, 0x09, 0x42, 0x21, 0xc2, + 0xdd, 0x29, 0x1d, 0x0a, 0x0c, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x46, 0x69, 0x6c, 0x74, 0x65, + 0x72, 0x12, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x52, 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x42, 0x46, + 0x5a, 0x44, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, + 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x2f, + 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x74, 0x65, 0x73, 0x74, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, + 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_controller_storage_target_targettest_store_v1_target_proto_rawDescOnce sync.Once + file_controller_storage_target_targettest_store_v1_target_proto_rawDescData = file_controller_storage_target_targettest_store_v1_target_proto_rawDesc +) + +func file_controller_storage_target_targettest_store_v1_target_proto_rawDescGZIP() []byte { + file_controller_storage_target_targettest_store_v1_target_proto_rawDescOnce.Do(func() { + file_controller_storage_target_targettest_store_v1_target_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_target_targettest_store_v1_target_proto_rawDescData) + }) + return file_controller_storage_target_targettest_store_v1_target_proto_rawDescData +} + +var file_controller_storage_target_targettest_store_v1_target_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_controller_storage_target_targettest_store_v1_target_proto_goTypes = []interface{}{ + (*Target)(nil), // 0: controller.storage.target.targettest.store.v1.Target + (*timestamp.Timestamp)(nil), // 1: controller.storage.timestamp.v1.Timestamp +} +var file_controller_storage_target_targettest_store_v1_target_proto_depIdxs = []int32{ + 1, // 0: controller.storage.target.targettest.store.v1.Target.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 1, // 1: controller.storage.target.targettest.store.v1.Target.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_controller_storage_target_targettest_store_v1_target_proto_init() } +func file_controller_storage_target_targettest_store_v1_target_proto_init() { + if File_controller_storage_target_targettest_store_v1_target_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_storage_target_targettest_store_v1_target_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Target); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controller_storage_target_targettest_store_v1_target_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_target_targettest_store_v1_target_proto_goTypes, + DependencyIndexes: file_controller_storage_target_targettest_store_v1_target_proto_depIdxs, + MessageInfos: file_controller_storage_target_targettest_store_v1_target_proto_msgTypes, + }.Build() + File_controller_storage_target_targettest_store_v1_target_proto = out.File + file_controller_storage_target_targettest_store_v1_target_proto_rawDesc = nil + file_controller_storage_target_targettest_store_v1_target_proto_goTypes = nil + file_controller_storage_target_targettest_store_v1_target_proto_depIdxs = nil +} diff --git a/internal/target/targettest/target.go b/internal/target/targettest/target.go new file mode 100644 index 000000000..4109fa465 --- /dev/null +++ b/internal/target/targettest/target.go @@ -0,0 +1,262 @@ +// Package targettest provides a test target subtype for use by the target +// package. Note that it leverages the tcp.Target's database table to avoid +// needing schema migrations just for tests. +package targettest + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/hashicorp/boundary/internal/target" + "github.com/hashicorp/boundary/internal/target/targettest/store" + "github.com/hashicorp/boundary/internal/types/subtypes" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +const ( + Subtype = subtypes.Subtype("tcp") + TargetPrefix = "ttcp" +) + +// Target is a target.Target used for tests. +type Target struct { + *store.Target + tableName string `gorm:"-"` +} + +var ( + _ target.Target = (*Target)(nil) + _ db.VetForWriter = (*Target)(nil) + _ oplog.ReplayableMessage = (*Target)(nil) +) + +// VetForWrite implements db.VetForWrite() interface and validates the tcp target +// before it's written. +func (t *Target) VetForWrite(ctx context.Context, _ db.Reader, opType db.OpType, _ ...db.Option) error { + const op = "target_test.(Target).VetForWrite" + if t.PublicId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing public id") + } + if opType == db.CreateOp { + if t.ScopeId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing scope id") + } + if t.Name == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing name") + } + } + return nil +} + +// TableName returns the tablename to override the default gorm table name +func (t *Target) TableName() string { + if t.tableName != "" { + return t.tableName + } + return "target_tcp" +} + +// SetTableName sets the tablename and satisfies the ReplayableMessage +// interface. If the caller attempts to set the name to "" the name will be +// reset to the default name. +func (t *Target) SetTableName(n string) { + t.tableName = n +} + +func (t *Target) GetPublicId() string { + return t.PublicId +} + +func (t *Target) GetScopeId() string { + return t.ScopeId +} + +func (t *Target) GetDefaultPort() uint32 { + return t.DefaultPort +} + +func (t *Target) GetName() string { + return t.Name +} + +func (t *Target) GetDescription() string { + return t.Description +} + +func (t *Target) GetVersion() uint32 { + return t.Version +} + +func (t *Target) GetType() subtypes.Subtype { + return Subtype +} + +func (t *Target) GetCreateTime() *timestamp.Timestamp { + return t.CreateTime +} + +func (t *Target) GetUpdateTime() *timestamp.Timestamp { + return t.UpdateTime +} + +func (t *Target) GetSessionMaxSeconds() uint32 { + return t.SessionMaxSeconds +} + +func (t *Target) GetSessionConnectionLimit() int32 { + return t.SessionConnectionLimit +} + +func (t *Target) GetWorkerFilter() string { + return t.WorkerFilter +} + +func (t *Target) Clone() target.Target { + cp := proto.Clone(t.Target) + return &Target{ + Target: cp.(*store.Target), + } +} + +func (t *Target) SetPublicId(_ context.Context, publicId string) error { + t.PublicId = publicId + return nil +} + +func (t *Target) SetScopeId(scopeId string) { + t.ScopeId = scopeId +} + +func (t *Target) SetName(name string) { + t.Name = name +} + +func (t *Target) SetDescription(description string) { + t.Description = description +} + +func (t *Target) SetVersion(v uint32) { + t.Version = v +} + +func (t *Target) SetDefaultPort(port uint32) { + t.DefaultPort = port +} + +func (t *Target) SetCreateTime(ts *timestamp.Timestamp) { + t.CreateTime = ts +} + +func (t *Target) SetUpdateTime(ts *timestamp.Timestamp) { + t.UpdateTime = ts +} + +func (t *Target) SetSessionMaxSeconds(s uint32) { + t.SessionMaxSeconds = s +} + +func (t *Target) SetSessionConnectionLimit(l int32) { + t.SessionConnectionLimit = l +} + +func (t *Target) SetWorkerFilter(f string) { + t.WorkerFilter = f +} + +func (t *Target) Oplog(op oplog.OpType) oplog.Metadata { + return oplog.Metadata{ + "resource-public-id": []string{t.PublicId}, + "resource-type": []string{"tcp target"}, + "op-type": []string{op.String()}, + "scope-id": []string{t.ScopeId}, + } +} + +// Alloc creates an in-memory Target. +func Alloc() target.Target { + return &Target{ + Target: &store.Target{}, + } +} + +// Vet checks that the given Target is a targettest.Target and that it is not nil. +func Vet(ctx context.Context, t target.Target) error { + const op = "targettest.Vet" + + tt, ok := t.(*Target) + if !ok { + return errors.New(ctx, errors.InvalidParameter, op, "target is not a tcp.Target") + } + + if tt == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing target") + } + + if tt.Target == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing target store") + } + return nil +} + +// New creates a targettest.Target. +func New(scopeId string, opt ...target.Option) (*Target, error) { + const op = "target_test.New" + opts := target.GetOpts(opt...) + if scopeId == "" { + return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing scope id") + } + t := &Target{ + Target: &store.Target{ + ScopeId: scopeId, + Name: opts.WithName, + Description: opts.WithDescription, + DefaultPort: opts.WithDefaultPort, + SessionConnectionLimit: opts.WithSessionConnectionLimit, + SessionMaxSeconds: opts.WithSessionMaxSeconds, + WorkerFilter: opts.WithWorkerFilter, + }, + } + return t, nil +} + +// TestNewTestTarget is a test helper for creating a targettest.Target. +func TestNewTestTarget(t *testing.T, conn *db.DB, scopeId, name string, opt ...target.Option) *Target { + t.Helper() + opt = append(opt, target.WithName(name)) + opts := target.GetOpts(opt...) + require := require.New(t) + rw := db.New(conn) + tar, err := New(scopeId, opt...) + require.NoError(err) + id, err := db.NewPublicId(TargetPrefix) + require.NoError(err) + tar.PublicId = id + err = rw.Create(context.Background(), tar) + require.NoError(err) + + if len(opts.WithHostSources) > 0 { + newHostSets := make([]interface{}, 0, len(opts.WithHostSources)) + for _, s := range opts.WithHostSources { + hostSet, err := target.NewTargetHostSet(tar.PublicId, s) + require.NoError(err) + newHostSets = append(newHostSets, hostSet) + } + err := rw.CreateItems(context.Background(), newHostSets) + require.NoError(err) + } + if len(opts.WithCredentialLibraries) > 0 { + newCredLibs := make([]interface{}, 0, len(opts.WithCredentialLibraries)) + for _, cl := range opts.WithCredentialLibraries { + cl.TargetId = tar.PublicId + newCredLibs = append(newCredLibs, cl) + } + err := rw.CreateItems(context.Background(), newCredLibs) + require.NoError(err) + } + return tar +} diff --git a/internal/target/tcp/repository_credential_source_test.go b/internal/target/tcp/repository_credential_source_test.go index ba0720773..7d1077c14 100644 --- a/internal/target/tcp/repository_credential_source_test.go +++ b/internal/target/tcp/repository_credential_source_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/credential/vault" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" @@ -106,7 +107,12 @@ func TestRepository_AddTargetCredentialSources(t *testing.T) { assert, require := assert.New(t), require.New(t) projTarget := tcp.TestTarget(t, conn, staticProj.PublicId, tt.name) - gotTarget, _, gotCredSources, err := repo.AddTargetCredentialSources(context.Background(), projTarget.PublicId, tt.args.targetVersion, tt.args.credLibIds) + credLibs := make([]*target.CredentialLibrary, 0, len(tt.args.credLibIds)) + for _, clid := range tt.args.credLibIds { + credLibs = append(credLibs, target.TestNewCredentialLibrary(projTarget.PublicId, clid, credential.ApplicationPurpose)) + } + + gotTarget, _, gotCredSources, err := repo.AddTargetCredentialSources(context.Background(), projTarget.PublicId, tt.args.targetVersion, credLibs) if tt.wantErr { require.Error(err) assert.Truef(errors.Match(errors.T(tt.wantErrCode), err), "unexpected error %s", err.Error()) @@ -142,18 +148,22 @@ func TestRepository_AddTargetCredentialSources(t *testing.T) { assert, require := assert.New(t), require.New(t) projTarget := tcp.TestTarget(t, conn, staticProj.PublicId, "add-existing") - _, _, gotCredSources, err := repo.AddTargetCredentialSources(context.Background(), projTarget.PublicId, 1, []string{lib1.PublicId}) + cl1 := target.TestNewCredentialLibrary(projTarget.PublicId, lib1.PublicId, credential.ApplicationPurpose) + cl2 := target.TestNewCredentialLibrary(projTarget.PublicId, lib2.PublicId, credential.ApplicationPurpose) + cl3 := target.TestNewCredentialLibrary(projTarget.PublicId, lib3.PublicId, credential.ApplicationPurpose) + + _, _, gotCredSources, err := repo.AddTargetCredentialSources(context.Background(), projTarget.PublicId, 1, []*target.CredentialLibrary{cl1}) 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}) + _, _, _, err = repo.AddTargetCredentialSources(context.Background(), projTarget.PublicId, 2, []*target.CredentialLibrary{cl1}) 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}) + _, _, _, err = repo.AddTargetCredentialSources(context.Background(), projTarget.PublicId, 2, []*target.CredentialLibrary{cl3, cl2, cl1}) require.Error(err) assert.True(errors.Match(errors.T(errors.NotUnique), err)) @@ -166,7 +176,8 @@ func TestRepository_AddTargetCredentialSources(t *testing.T) { 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}) + cl1 := target.TestNewCredentialLibrary("fake-target-id", lib1.PublicId, credential.ApplicationPurpose) + _, _, _, err := repo.AddTargetCredentialSources(context.Background(), "fake-target-id", 1, []*target.CredentialLibrary{cl1}) require.Error(err) assert.Truef(errors.Match(errors.T(errors.RecordNotFound), err), "unexpected error %s", err.Error()) @@ -275,29 +286,29 @@ func TestRepository_DeleteTargetCredentialSources(t *testing.T) { assert, require := assert.New(t), require.New(t) cs := css[i] - target := tcp.TestTarget(t, conn, proj.PublicId, tt.name) + tar := tcp.TestTarget(t, conn, proj.PublicId, tt.name) - clIds := make([]string, 0, tt.args.createCnt) + cls := make([]*target.CredentialLibrary, 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) + cls = append(cls, target.TestNewCredentialLibrary(tar.GetPublicId(), cl.PublicId, credential.ApplicationPurpose)) } } - _, _, addedCredSources, err := repo.AddTargetCredentialSources(context.Background(), target.GetPublicId(), 1, clIds) + _, _, addedCredSources, err := repo.AddTargetCredentialSources(context.Background(), tar.GetPublicId(), 1, cls) require.NoError(err) assert.Equal(tt.args.createCnt, len(addedCredSources)) - deleteCredSources := make([]string, 0, tt.args.deleteCnt) + deleteCredSources := make([]*target.CredentialLibrary, 0, tt.args.deleteCnt) for i := 0; i < tt.args.deleteCnt; i++ { - deleteCredSources = append(deleteCredSources, clIds[i]) + deleteCredSources = append(deleteCredSources, cls[i]) } var targetId string switch { case tt.args.targetIdOverride != nil: targetId = *tt.args.targetIdOverride default: - targetId = target.GetPublicId() + targetId = tar.GetPublicId() } var targetVersion uint32 switch { @@ -317,7 +328,7 @@ func TestRepository_DeleteTargetCredentialSources(t *testing.T) { 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)) + err = db.TestVerifyOplog(t, rw, tar.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second)) assert.NoError(err) }) } @@ -333,18 +344,22 @@ func TestRepository_DeleteTargetCredentialSources(t *testing.T) { lib3 := libs[2] projTarget := tcp.TestTarget(t, conn, proj.PublicId, "add-existing") - _, _, gotCredSources, err := repo.AddTargetCredentialSources(context.Background(), projTarget.PublicId, 1, []string{lib1.PublicId, lib2.PublicId}) + cl1 := target.TestNewCredentialLibrary(projTarget.PublicId, lib1.PublicId, credential.ApplicationPurpose) + cl2 := target.TestNewCredentialLibrary(projTarget.PublicId, lib2.PublicId, credential.ApplicationPurpose) + cl3 := target.TestNewCredentialLibrary(projTarget.PublicId, lib3.PublicId, credential.ApplicationPurpose) + + _, _, gotCredSources, err := repo.AddTargetCredentialSources(context.Background(), projTarget.PublicId, 1, []*target.CredentialLibrary{cl1, cl2}) 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}) + delCount, err := repo.DeleteTargetCredentialSources(context.Background(), projTarget.PublicId, 2, []*target.CredentialLibrary{cl3}) 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}) + delCount, err = repo.DeleteTargetCredentialSources(context.Background(), projTarget.PublicId, 2, []*target.CredentialLibrary{cl1, cl2, cl3}) require.Error(err) assert.True(errors.Match(errors.T(errors.MultipleRecords), err)) assert.Equal(0, delCount) @@ -373,17 +388,17 @@ func TestRepository_SetTargetCredentialSources(t *testing.T) { lib1 := credLibs[0] lib2 := credLibs[1] - setupFn := func(target target.Target) []target.CredentialSource { + setupFn := func(tar target.Target) ([]target.CredentialSource, []*target.CredentialLibrary) { credLibs := vault.TestCredentialLibraries(t, conn, wrapper, cs.GetPublicId(), 10) - clIds := make([]string, 0, len(credLibs)) + cls := make([]*target.CredentialLibrary, 0, len(credLibs)) for _, cl := range credLibs { - clIds = append(clIds, cl.PublicId) + cls = append(cls, target.TestNewCredentialLibrary(tar.GetPublicId(), cl.PublicId, credential.ApplicationPurpose)) } - _, _, created, err := repo.AddTargetCredentialSources(context.Background(), target.GetPublicId(), 1, clIds) + _, _, created, err := repo.AddTargetCredentialSources(context.Background(), tar.GetPublicId(), 1, cls) require.NoError(t, err) require.Equal(t, 10, len(created)) - return created + return created, cls } type args struct { targetVersion uint32 @@ -392,7 +407,7 @@ func TestRepository_SetTargetCredentialSources(t *testing.T) { } tests := []struct { name string - setup func(target.Target) []target.CredentialSource + setup func(target.Target) ([]target.CredentialSource, []*target.CredentialLibrary) args args wantAffectedRows int wantErr bool @@ -471,22 +486,29 @@ func TestRepository_SetTargetCredentialSources(t *testing.T) { tar := tcp.TestTarget(t, conn, proj.PublicId, tt.name) var origCredSources []target.CredentialSource + var origCredLibraries []*target.CredentialLibrary if tt.setup != nil { - origCredSources = tt.setup(tar) + origCredSources, origCredLibraries = tt.setup(tar) } + + cls := make([]*target.CredentialLibrary, 0, len(tt.args.clIds)) + for _, clid := range tt.args.clIds { + cls = append(cls, target.TestNewCredentialLibrary(tar.GetPublicId(), clid, credential.ApplicationPurpose)) + } + var wantIds []string + wantIds = append(wantIds, tt.args.clIds...) if tt.args.addToOrigLibs { - origIds := make([]string, 0, len(origCredSources)) - for _, cl := range origCredSources { - origIds = append(origIds, cl.Id()) + cls = append(cls, origCredLibraries...) + for _, cl := range origCredLibraries { + wantIds = append(wantIds, cl.CredentialLibraryId) } - tt.args.clIds = append(tt.args.clIds, origIds...) } origTarget, _, lookupCredSources, err := repo.LookupTarget(context.Background(), tar.GetPublicId()) require.NoError(err) assert.Equal(origCredSources, lookupCredSources) - _, got, affectedRows, err := repo.SetTargetCredentialSources(context.Background(), tar.GetPublicId(), tt.args.targetVersion, tt.args.clIds) + _, got, affectedRows, err := repo.SetTargetCredentialSources(context.Background(), tar.GetPublicId(), tt.args.targetVersion, cls) if tt.wantErr { require.Error(err) assert.Equal(0, affectedRows) @@ -496,10 +518,8 @@ func TestRepository_SetTargetCredentialSources(t *testing.T) { 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...) + assert.Equal(len(wantIds), len(got)) sort.Strings(wantIds) var gotIds []string @@ -523,7 +543,8 @@ func TestRepository_SetTargetCredentialSources(t *testing.T) { 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}) + cl1 := target.TestNewCredentialLibrary("", lib1.PublicId, credential.ApplicationPurpose) + _, _, _, err := repo.SetTargetCredentialSources(context.Background(), "", 1, []*target.CredentialLibrary{cl1}) require.Error(err) assert.Truef(errors.Match(errors.T(errors.InvalidParameter), err), "unexpected error %s", err.Error()) @@ -531,7 +552,8 @@ func TestRepository_SetTargetCredentialSources(t *testing.T) { 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}) + cl1 := target.TestNewCredentialLibrary("fake-target-id", lib1.PublicId, credential.ApplicationPurpose) + _, _, _, err := repo.SetTargetCredentialSources(context.Background(), "fake-target-id", 1, []*target.CredentialLibrary{cl1}) require.Error(err) assert.Truef(errors.Match(errors.T(errors.RecordNotFound), err), "unexpected error %s", err.Error()) diff --git a/internal/target/tcp/repository_tcp_target_test.go b/internal/target/tcp/repository_tcp_target_test.go index 6ae271b5c..9b2338a67 100644 --- a/internal/target/tcp/repository_tcp_target_test.go +++ b/internal/target/tcp/repository_tcp_target_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/credential/vault" "github.com/hashicorp/boundary/internal/db" dbassert "github.com/hashicorp/boundary/internal/db/assert" @@ -14,6 +15,7 @@ import ( "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/oplog" "github.com/hashicorp/boundary/internal/target" + "github.com/hashicorp/boundary/internal/target/store" "github.com/hashicorp/boundary/internal/target/tcp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -39,9 +41,16 @@ func TestRepository_CreateTarget(t *testing.T) { cs := vault.TestCredentialStores(t, conn, wrapper, proj.GetPublicId(), 1)[0] credSources := vault.TestCredentialLibraries(t, conn, wrapper, cs.GetPublicId(), 2) - var clIds []string + var credLibraries []*target.CredentialLibrary + var credLibIds []string for _, cl := range credSources { - clIds = append(clIds, cl.PublicId) + credLibraries = append(credLibraries, &target.CredentialLibrary{ + CredentialLibrary: &store.CredentialLibrary{ + CredentialLibraryId: cl.PublicId, + CredentialPurpose: string(credential.ApplicationPurpose), + }, + }) + credLibIds = append(credLibIds, cl.PublicId) } type args struct { @@ -392,11 +401,22 @@ func TestRepository_UpdateTcpTarget(t *testing.T) { cls := vault.TestCredentialLibraries(t, conn, wrapper, cs.GetPublicId(), 5) var testClIds []string + var testCredLibs []*target.CredentialLibrary for _, cl := range cls { + testCredLibs = append(testCredLibs, &target.CredentialLibrary{ + CredentialLibrary: &store.CredentialLibrary{ + CredentialLibraryId: cl.PublicId, + CredentialPurpose: string(credential.ApplicationPurpose), + }, + }) testClIds = append(testClIds, cl.PublicId) } - tt.newTargetOpts = append(tt.newTargetOpts, target.WithHostSources(testHostSetIds), target.WithCredentialSources(testClIds)) + tt.newTargetOpts = append( + tt.newTargetOpts, + target.WithHostSources(testHostSetIds), + target.WithCredentialLibraries(testCredLibs), + ) name := tt.newName if name == "" { name = tcp.TestId(t) diff --git a/internal/target/tcp/testing.go b/internal/target/tcp/testing.go index f78f61e9b..6e917abd4 100644 --- a/internal/target/tcp/testing.go +++ b/internal/target/tcp/testing.go @@ -36,12 +36,11 @@ func TestTarget(t *testing.T, conn *db.DB, scopeId, name string, opt ...target.O err := rw.CreateItems(context.Background(), newHostSets) require.NoError(err) } - if len(opts.WithCredentialSources) > 0 { - newCredLibs := make([]interface{}, 0, len(opts.WithCredentialSources)) - for _, cl := range opts.WithCredentialSources { - newCl, err := target.NewCredentialLibrary(tar.PublicId, cl) - require.NoError(err) - newCredLibs = append(newCredLibs, newCl) + if len(opts.WithCredentialLibraries) > 0 { + newCredLibs := make([]interface{}, 0, len(opts.WithCredentialLibraries)) + for _, cl := range opts.WithCredentialLibraries { + cl.TargetId = tar.PublicId + newCredLibs = append(newCredLibs, cl) } err := rw.CreateItems(context.Background(), newCredLibs) require.NoError(err) diff --git a/internal/target/testing.go b/internal/target/testing.go index b436a7963..9460fd382 100644 --- a/internal/target/testing.go +++ b/internal/target/testing.go @@ -4,19 +4,33 @@ import ( "context" "testing" + "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/target/store" "github.com/stretchr/testify/require" ) +// TestNewCredentialLibrary creates a new in memory CredentialLibrary +// representing the relationship between targetId and credentialLibraryId with +// the given purpose. +func TestNewCredentialLibrary(targetId, credentialLibraryId string, purpose credential.Purpose) *CredentialLibrary { + return &CredentialLibrary{ + CredentialLibrary: &store.CredentialLibrary{ + TargetId: targetId, + CredentialLibraryId: credentialLibraryId, + CredentialPurpose: string(purpose), + }, + } +} + // TestCredentialLibrary creates a CredentialLibrary for targetId and -// libraryId. +// libraryId with the credential purpose of application. func TestCredentialLibrary(t *testing.T, conn *db.DB, targetId, libraryId string) *CredentialLibrary { t.Helper() require := require.New(t) rw := db.New(conn) - lib, err := NewCredentialLibrary(targetId, libraryId) - require.NoError(err) - err = rw.Create(context.Background(), lib) + lib := TestNewCredentialLibrary(targetId, libraryId, credential.ApplicationPurpose) + err := rw.Create(context.Background(), lib) require.NoError(err) return lib }