// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package credentials import ( "context" "encoding/base64" "encoding/json" "fmt" "slices" "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/authtoken" "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/boundary/internal/credential/static" "github.com/hashicorp/boundary/internal/credential/static/store" "github.com/hashicorp/boundary/internal/credential/vault" "github.com/hashicorp/boundary/internal/daemon/controller/auth" "github.com/hashicorp/boundary/internal/daemon/controller/handlers" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/libs/crypto" "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/scheduler" "github.com/hashicorp/boundary/internal/server" "github.com/hashicorp/boundary/internal/types/scope" pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/credentials" scopepb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/crypto/ssh/testdata" "google.golang.org/grpc/codes" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/fieldmaskpb" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/wrapperspb" ) var testAuthorizedActions = []string{"no-op", "read", "update", "delete"} func staticJsonCredentialToProto(cred *static.JsonCredential, prj *iam.Scope, hmac string) *pb.Credential { return &pb.Credential{ Id: cred.GetPublicId(), CredentialStoreId: cred.GetStoreId(), Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, CreatedTime: cred.GetCreateTime().GetTimestamp(), UpdatedTime: cred.GetUpdateTime().GetTimestamp(), Version: cred.GetVersion(), Type: credential.JsonSubtype.String(), AuthorizedActions: testAuthorizedActions, Attrs: &pb.Credential_JsonAttributes{ JsonAttributes: &pb.JsonAttributes{ ObjectHmac: base64.RawURLEncoding.EncodeToString([]byte(hmac)), }, }, } } func staticPasswordCredentialToProto(cred *static.UsernamePasswordCredential, prj *iam.Scope, hmac string) *pb.Credential { return &pb.Credential{ Id: cred.GetPublicId(), CredentialStoreId: cred.GetStoreId(), Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, CreatedTime: cred.GetCreateTime().GetTimestamp(), UpdatedTime: cred.GetUpdateTime().GetTimestamp(), Version: cred.GetVersion(), Type: credential.UsernamePasswordSubtype.String(), AuthorizedActions: testAuthorizedActions, Attrs: &pb.Credential_UsernamePasswordAttributes{ UsernamePasswordAttributes: &pb.UsernamePasswordAttributes{ Username: wrapperspb.String(cred.GetUsername()), PasswordHmac: base64.RawURLEncoding.EncodeToString([]byte(hmac)), }, }, } } func staticSshCredentialToProto(cred *static.SshPrivateKeyCredential, prj *iam.Scope, hmac string) *pb.Credential { return &pb.Credential{ Id: cred.GetPublicId(), CredentialStoreId: cred.GetStoreId(), Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, CreatedTime: cred.GetCreateTime().GetTimestamp(), UpdatedTime: cred.GetUpdateTime().GetTimestamp(), Version: cred.GetVersion(), Type: credential.SshPrivateKeySubtype.String(), AuthorizedActions: testAuthorizedActions, Attrs: &pb.Credential_SshPrivateKeyAttributes{ SshPrivateKeyAttributes: &pb.SshPrivateKeyAttributes{ Username: wrapperspb.String(cred.GetUsername()), PrivateKeyHmac: base64.RawURLEncoding.EncodeToString([]byte(hmac)), }, }, } } func TestList(t *testing.T) { ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") wrapper := db.TestWrapper(t) kkms := kms.TestKms(t, conn, wrapper) rw := db.New(conn) iamRepo := iam.TestRepo(t, conn, wrapper) iamRepoFn := func() (*iam.Repository, error) { return iamRepo, nil } staticRepoFn := func() (*static.Repository, error) { return static.NewRepository(ctx, rw, rw, kkms) } _, prj := iam.TestScopes(t, iamRepo) store := static.TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) storeNoCreds := static.TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) databaseWrapper, err := kkms.GetWrapper(ctx, prj.GetPublicId(), kms.KeyPurposeDatabase) require.NoError(t, err) var wantCreds []*pb.Credential for i := 0; i < 10; i++ { user := fmt.Sprintf("user-%d", i) pass := fmt.Sprintf("pass-%d", i) c := static.TestUsernamePasswordCredential(t, conn, wrapper, user, pass, store.GetPublicId(), prj.GetPublicId()) hm, err := crypto.HmacSha256(ctx, []byte(pass), databaseWrapper, []byte(store.GetPublicId()), nil, crypto.WithEd25519()) require.NoError(t, err) wantCreds = append(wantCreds, staticPasswordCredentialToProto(c, prj, hm)) spk := static.TestSshPrivateKeyCredential(t, conn, wrapper, user, static.TestSshPrivateKeyPem, store.GetPublicId(), prj.GetPublicId()) hm, err = crypto.HmacSha256(ctx, []byte(static.TestSshPrivateKeyPem), databaseWrapper, []byte(store.GetPublicId()), nil) require.NoError(t, err) wantCreds = append(wantCreds, staticSshCredentialToProto(spk, prj, hm)) obj, objBytes := static.TestJsonObject(t) credJson := static.TestJsonCredential(t, conn, wrapper, store.GetPublicId(), prj.GetPublicId(), obj) hm, err = crypto.HmacSha256(ctx, objBytes, databaseWrapper, []byte(store.GetPublicId()), nil) require.NoError(t, err) wantCreds = append(wantCreds, staticJsonCredentialToProto(credJson, prj, hm)) } cases := []struct { name string req *pbs.ListCredentialsRequest res *pbs.ListCredentialsResponse anonRes *pbs.ListCredentialsResponse err error }{ { name: "List many credentials", req: &pbs.ListCredentialsRequest{CredentialStoreId: store.GetPublicId()}, res: &pbs.ListCredentialsResponse{ Items: wantCreds, ResponseType: "complete", ListToken: "", SortBy: "created_time", SortDir: "desc", RemovedIds: nil, EstItemCount: 30, }, anonRes: &pbs.ListCredentialsResponse{ Items: wantCreds, ResponseType: "complete", ListToken: "", SortBy: "created_time", SortDir: "desc", RemovedIds: nil, EstItemCount: 30, }, }, { name: "List no credentials", req: &pbs.ListCredentialsRequest{CredentialStoreId: storeNoCreds.GetPublicId()}, res: &pbs.ListCredentialsResponse{ ResponseType: "complete", ListToken: "", SortBy: "created_time", SortDir: "desc", RemovedIds: nil, }, anonRes: &pbs.ListCredentialsResponse{ ResponseType: "complete", ListToken: "", SortBy: "created_time", SortDir: "desc", RemovedIds: nil, }, }, { name: "Filter to one credential", req: &pbs.ListCredentialsRequest{CredentialStoreId: store.GetPublicId(), Filter: fmt.Sprintf(`"/item/id"==%q`, wantCreds[1].GetId())}, res: &pbs.ListCredentialsResponse{ Items: wantCreds[1:2], ResponseType: "complete", ListToken: "", SortBy: "created_time", SortDir: "desc", RemovedIds: nil, EstItemCount: 1, }, anonRes: &pbs.ListCredentialsResponse{ Items: wantCreds[1:2], ResponseType: "complete", ListToken: "", SortBy: "created_time", SortDir: "desc", RemovedIds: nil, EstItemCount: 1, }, }, { name: "Filter on Attribute", req: &pbs.ListCredentialsRequest{CredentialStoreId: store.GetPublicId(), Filter: fmt.Sprintf(`"/item/attributes/username"==%q`, wantCreds[3].GetUsernamePasswordAttributes().GetUsername().Value)}, res: &pbs.ListCredentialsResponse{ Items: wantCreds[3:5], ResponseType: "complete", ListToken: "", SortBy: "created_time", SortDir: "desc", RemovedIds: nil, EstItemCount: 2, }, anonRes: &pbs.ListCredentialsResponse{ ResponseType: "complete", ListToken: "", SortBy: "created_time", SortDir: "desc", RemovedIds: nil, }, // anonymous user does not have access to attributes }, { name: "Filter to no credential", req: &pbs.ListCredentialsRequest{CredentialStoreId: store.GetPublicId(), Filter: `"/item/id"=="doesnt match"`}, res: &pbs.ListCredentialsResponse{ ResponseType: "complete", ListToken: "", SortBy: "created_time", SortDir: "desc", RemovedIds: nil, }, anonRes: &pbs.ListCredentialsResponse{ ResponseType: "complete", ListToken: "", SortBy: "created_time", SortDir: "desc", RemovedIds: nil, }, }, { name: "Filter Bad Format", req: &pbs.ListCredentialsRequest{CredentialStoreId: store.GetPublicId(), Filter: `"//id/"=="bad"`}, err: handlers.InvalidArgumentErrorf("bad format", nil), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { s, err := NewService(ctx, iamRepoFn, staticRepoFn, 1000) require.NoError(t, err, "Couldn't create new host set service.") // Test non-anonymous listing got, gErr := s.ListCredentials(auth.DisabledAuthTestContext(iamRepoFn, prj.GetPublicId()), tc.req) if tc.err != nil { require.Error(t, gErr) assert.True(t, errors.Is(gErr, tc.err), "ListCredentialStore(%q) got error %v, wanted %v", tc.req, gErr, tc.err) return } require.NoError(t, gErr) assert.Empty( t, cmp.Diff( got, tc.res, protocmp.Transform(), protocmp.SortRepeated(func(x, y *pb.Credential) bool { return x.Id < y.Id }), cmpopts.SortSlices(func(a, b string) bool { return a < b }), protocmp.IgnoreFields(&pbs.ListCredentialsResponse{}, "list_token"), ), ) // Test anonymous listing got, gErr = s.ListCredentials(auth.DisabledAuthTestContext(iamRepoFn, prj.GetPublicId(), auth.WithUserId(globals.AnonymousUserId)), tc.req) require.NoError(t, gErr) assert.Len(t, got.Items, len(tc.anonRes.Items)) for _, item := range got.GetItems() { require.Nil(t, item.CreatedTime) require.Nil(t, item.UpdatedTime) require.Zero(t, item.Version) } }) } } func TestGet(t *testing.T) { ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") wrapper := db.TestWrapper(t) kkms := kms.TestKms(t, conn, wrapper) rw := db.New(conn) iamRepo := iam.TestRepo(t, conn, wrapper) iamRepoFn := func() (*iam.Repository, error) { return iamRepo, nil } staticRepoFn := func() (*static.Repository, error) { return static.NewRepository(context.Background(), rw, rw, kkms) } _, prj := iam.TestScopes(t, iamRepo) databaseWrapper, err := kkms.GetWrapper(context.Background(), prj.GetPublicId(), kms.KeyPurposeDatabase) require.NoError(t, err) store := static.TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) s, err := NewService(ctx, iamRepoFn, staticRepoFn, 1000) require.NoError(t, err) upCred := static.TestUsernamePasswordCredential(t, conn, wrapper, "user", "pass", store.GetPublicId(), prj.GetPublicId()) upCredPrev := static.TestUsernamePasswordCredential(t, conn, wrapper, "user", "pass", store.GetPublicId(), prj.GetPublicId(), static.WithPublicId(fmt.Sprintf("%s_1234567890", globals.UsernamePasswordCredentialPreviousPrefix))) upHm, err := crypto.HmacSha256(context.Background(), []byte("pass"), databaseWrapper, []byte(store.GetPublicId()), nil, crypto.WithEd25519()) require.NoError(t, err) spkCred := static.TestSshPrivateKeyCredential(t, conn, wrapper, "user", static.TestSshPrivateKeyPem, store.GetPublicId(), prj.GetPublicId()) spkHm, err := crypto.HmacSha256(context.Background(), []byte(static.TestSshPrivateKeyPem), databaseWrapper, []byte(store.GetPublicId()), nil) require.NoError(t, err) spkCredWithPass := static.TestSshPrivateKeyCredential(t, conn, wrapper, "user", string(testdata.PEMEncryptedKeys[0].PEMBytes), store.GetPublicId(), prj.GetPublicId(), static.WithPrivateKeyPassphrase([]byte(testdata.PEMEncryptedKeys[0].EncryptionKey))) spkWithPassHm, err := crypto.HmacSha256(context.Background(), []byte(testdata.PEMEncryptedKeys[0].PEMBytes), databaseWrapper, []byte(store.GetPublicId()), nil) require.NoError(t, err) passHm, err := crypto.HmacSha256(context.Background(), []byte(testdata.PEMEncryptedKeys[0].EncryptionKey), databaseWrapper, []byte(store.GetPublicId()), nil) require.NoError(t, err) obj, objBytes := static.TestJsonObject(t) jsonCred := static.TestJsonCredential(t, conn, wrapper, store.GetPublicId(), prj.GetPublicId(), obj) objectHmac, err := crypto.HmacSha256(context.Background(), objBytes, databaseWrapper, []byte(store.GetPublicId()), nil) require.NoError(t, err) cases := []struct { name string id string res *pbs.GetCredentialResponse err error }{ { name: "success-up", id: upCred.GetPublicId(), res: &pbs.GetCredentialResponse{ Item: &pb.Credential{ Id: upCred.GetPublicId(), CredentialStoreId: upCred.GetStoreId(), Scope: &scopepb.ScopeInfo{Id: store.GetProjectId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, Type: credential.UsernamePasswordSubtype.String(), AuthorizedActions: testAuthorizedActions, CreatedTime: upCred.CreateTime.GetTimestamp(), UpdatedTime: upCred.UpdateTime.GetTimestamp(), Version: 1, Attrs: &pb.Credential_UsernamePasswordAttributes{ UsernamePasswordAttributes: &pb.UsernamePasswordAttributes{ Username: wrapperspb.String("user"), PasswordHmac: base64.RawURLEncoding.EncodeToString([]byte(upHm)), }, }, }, }, }, { name: "success-up-prev-prefix", id: upCredPrev.GetPublicId(), res: &pbs.GetCredentialResponse{ Item: &pb.Credential{ Id: upCredPrev.GetPublicId(), CredentialStoreId: upCred.GetStoreId(), Scope: &scopepb.ScopeInfo{Id: store.GetProjectId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, Type: credential.UsernamePasswordSubtype.String(), AuthorizedActions: testAuthorizedActions, CreatedTime: upCredPrev.CreateTime.GetTimestamp(), UpdatedTime: upCredPrev.UpdateTime.GetTimestamp(), Version: 1, Attrs: &pb.Credential_UsernamePasswordAttributes{ UsernamePasswordAttributes: &pb.UsernamePasswordAttributes{ Username: wrapperspb.String("user"), PasswordHmac: base64.RawURLEncoding.EncodeToString([]byte(upHm)), }, }, }, }, }, { name: "success-spk", id: spkCred.GetPublicId(), res: &pbs.GetCredentialResponse{ Item: &pb.Credential{ Id: spkCred.GetPublicId(), CredentialStoreId: spkCred.GetStoreId(), Scope: &scopepb.ScopeInfo{Id: store.GetProjectId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, Type: credential.SshPrivateKeySubtype.String(), AuthorizedActions: testAuthorizedActions, CreatedTime: spkCred.CreateTime.GetTimestamp(), UpdatedTime: spkCred.UpdateTime.GetTimestamp(), Version: 1, Attrs: &pb.Credential_SshPrivateKeyAttributes{ SshPrivateKeyAttributes: &pb.SshPrivateKeyAttributes{ Username: wrapperspb.String("user"), PrivateKeyHmac: base64.RawURLEncoding.EncodeToString([]byte(spkHm)), }, }, }, }, }, { name: "success-spk-with-pass", id: spkCredWithPass.GetPublicId(), res: &pbs.GetCredentialResponse{ Item: &pb.Credential{ Id: spkCredWithPass.GetPublicId(), CredentialStoreId: spkCredWithPass.GetStoreId(), Scope: &scopepb.ScopeInfo{Id: store.GetProjectId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, Type: credential.SshPrivateKeySubtype.String(), AuthorizedActions: testAuthorizedActions, CreatedTime: spkCredWithPass.CreateTime.GetTimestamp(), UpdatedTime: spkCredWithPass.UpdateTime.GetTimestamp(), Version: 1, Attrs: &pb.Credential_SshPrivateKeyAttributes{ SshPrivateKeyAttributes: &pb.SshPrivateKeyAttributes{ Username: wrapperspb.String("user"), PrivateKeyHmac: base64.RawURLEncoding.EncodeToString([]byte(spkWithPassHm)), PrivateKeyPassphraseHmac: base64.RawURLEncoding.EncodeToString([]byte(passHm)), }, }, }, }, }, { name: "success-json", id: jsonCred.GetPublicId(), res: &pbs.GetCredentialResponse{ Item: &pb.Credential{ Id: jsonCred.GetPublicId(), CredentialStoreId: jsonCred.GetStoreId(), Scope: &scopepb.ScopeInfo{Id: store.GetProjectId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, Type: credential.JsonSubtype.String(), AuthorizedActions: testAuthorizedActions, CreatedTime: jsonCred.CreateTime.GetTimestamp(), UpdatedTime: jsonCred.UpdateTime.GetTimestamp(), Version: 1, Attrs: &pb.Credential_JsonAttributes{ JsonAttributes: &pb.JsonAttributes{ ObjectHmac: base64.RawURLEncoding.EncodeToString([]byte(objectHmac)), }, }, }, }, }, { name: "not found error", id: fmt.Sprintf("%s_1234567890", globals.UsernamePasswordCredentialPrefix), err: handlers.NotFoundError(), }, { name: "bad prefix", id: fmt.Sprintf("%s_1234567890", globals.StaticCredentialStorePrefix), err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { req := &pbs.GetCredentialRequest{Id: tc.id} // Test non-anonymous get got, gErr := s.GetCredential(auth.DisabledAuthTestContext(iamRepoFn, prj.GetPublicId()), req) if tc.err != nil { require.Error(t, gErr) assert.True(t, errors.Is(gErr, tc.err)) return } require.NoError(t, gErr) assert.Empty(t, cmp.Diff( got, tc.res, protocmp.Transform(), cmpopts.SortSlices(func(a, b string) bool { return a < b }), )) // Test anonymous get got, gErr = s.GetCredential(auth.DisabledAuthTestContext(iamRepoFn, prj.GetPublicId(), auth.WithUserId(globals.AnonymousUserId)), req) require.NoError(t, gErr) require.Nil(t, got.Item.CreatedTime) require.Nil(t, got.Item.UpdatedTime) require.Zero(t, got.Item.Version) }) } } func TestDelete(t *testing.T) { ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") wrapper := db.TestWrapper(t) kms := kms.TestKms(t, conn, wrapper) sche := scheduler.TestScheduler(t, conn, wrapper) rw := db.New(conn) err := vault.RegisterJobs(context.Background(), sche, rw, rw, kms) require.NoError(t, err) iamRepo := iam.TestRepo(t, conn, wrapper) iamRepoFn := func() (*iam.Repository, error) { return iamRepo, nil } staticRepoFn := func() (*static.Repository, error) { return static.NewRepository(context.Background(), rw, rw, kms) } _, prj := iam.TestScopes(t, iamRepo) store := static.TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) s, err := NewService(ctx, iamRepoFn, staticRepoFn, 1000) require.NoError(t, err) upCred := static.TestUsernamePasswordCredential(t, conn, wrapper, "user", "pass", store.GetPublicId(), prj.GetPublicId()) spkCred := static.TestSshPrivateKeyCredential(t, conn, wrapper, "user", static.TestSshPrivateKeyPem, store.GetPublicId(), prj.GetPublicId()) obj, _ := static.TestJsonObject(t) jsonCred := static.TestJsonCredential(t, conn, wrapper, store.GetPublicId(), prj.GetPublicId(), obj) cases := []struct { name string id string err error res *pbs.DeleteCredentialResponse }{ { name: "success-up", id: upCred.GetPublicId(), }, { name: "success-spk", id: spkCred.GetPublicId(), }, { name: "success-json", id: jsonCred.GetPublicId(), }, { name: "not found error", id: fmt.Sprintf("%s_1234567890", globals.UsernamePasswordCredentialPrefix), err: handlers.NotFoundError(), }, { name: "bad prefix", id: fmt.Sprintf("%s_1234567890", globals.StaticCredentialStorePrefix), err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got, gErr := s.DeleteCredential(auth.DisabledAuthTestContext(iamRepoFn, prj.GetPublicId()), &pbs.DeleteCredentialRequest{Id: tc.id}) assert.EqualValuesf(t, tc.res, got, "DeleteCredentialStore got response %q, wanted %q", got, tc.res) if tc.err != nil { require.Error(t, gErr) assert.True(t, errors.Is(gErr, tc.err)) return } require.NoError(t, gErr) g, err := s.GetCredential(auth.DisabledAuthTestContext(iamRepoFn, prj.GetPublicId()), &pbs.GetCredentialRequest{Id: tc.id}) assert.Nil(t, g) assert.True(t, errors.Is(err, handlers.NotFoundError())) }) } } func TestCreate(t *testing.T) { ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") wrapper := db.TestWrapper(t) kkms := kms.TestKms(t, conn, wrapper) rw := db.New(conn) iamRepo := iam.TestRepo(t, conn, wrapper) iamRepoFn := func() (*iam.Repository, error) { return iamRepo, nil } repoFn := func() (*static.Repository, error) { return static.NewRepository(context.Background(), rw, rw, kkms) } _, prj := iam.TestScopes(t, iamRepo) store := static.TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) obj, objBytes := static.TestJsonObject(t) cases := []struct { name string req *pbs.CreateCredentialRequest res *pbs.CreateCredentialResponse idPrefix string err error wantErr bool }{ { name: "Can't specify Id", req: &pbs.CreateCredentialRequest{Item: &pb.Credential{ CredentialStoreId: store.GetPublicId(), Id: globals.UsernamePasswordCredentialPrefix + "_notallowed", Type: credential.UsernamePasswordSubtype.String(), Attrs: &pb.Credential_UsernamePasswordAttributes{ UsernamePasswordAttributes: &pb.UsernamePasswordAttributes{ Username: wrapperspb.String("username"), Password: wrapperspb.String("password"), }, }, }}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Invalid Credential Store Id", req: &pbs.CreateCredentialRequest{Item: &pb.Credential{ CredentialStoreId: "p_invalidid", Type: credential.UsernamePasswordSubtype.String(), Attrs: &pb.Credential_UsernamePasswordAttributes{ UsernamePasswordAttributes: &pb.UsernamePasswordAttributes{ Username: wrapperspb.String("username"), Password: wrapperspb.String("password"), }, }, }}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Can't specify Created Time", req: &pbs.CreateCredentialRequest{Item: &pb.Credential{ CredentialStoreId: store.GetPublicId(), CreatedTime: timestamppb.Now(), Type: credential.UsernamePasswordSubtype.String(), Attrs: &pb.Credential_UsernamePasswordAttributes{ UsernamePasswordAttributes: &pb.UsernamePasswordAttributes{ Username: wrapperspb.String("username"), }, }, }}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Can't specify Updated Time", req: &pbs.CreateCredentialRequest{Item: &pb.Credential{ CredentialStoreId: store.GetPublicId(), UpdatedTime: timestamppb.Now(), Type: credential.UsernamePasswordSubtype.String(), Attrs: &pb.Credential_UsernamePasswordAttributes{ UsernamePasswordAttributes: &pb.UsernamePasswordAttributes{ Username: wrapperspb.String("username"), Password: wrapperspb.String("password"), }, }, }}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Must provide type", req: &pbs.CreateCredentialRequest{Item: &pb.Credential{ CredentialStoreId: store.GetPublicId(), Attrs: &pb.Credential_UsernamePasswordAttributes{ UsernamePasswordAttributes: &pb.UsernamePasswordAttributes{ Username: wrapperspb.String("username"), Password: wrapperspb.String("password"), }, }, }}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Must provide username", req: &pbs.CreateCredentialRequest{Item: &pb.Credential{ CredentialStoreId: store.GetPublicId(), Type: credential.UsernamePasswordSubtype.String(), Attrs: &pb.Credential_UsernamePasswordAttributes{ UsernamePasswordAttributes: &pb.UsernamePasswordAttributes{ Password: wrapperspb.String("password"), }, }, }}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Must provide password", req: &pbs.CreateCredentialRequest{Item: &pb.Credential{ CredentialStoreId: store.GetPublicId(), Type: credential.UsernamePasswordSubtype.String(), Attrs: &pb.Credential_UsernamePasswordAttributes{ UsernamePasswordAttributes: &pb.UsernamePasswordAttributes{ Username: wrapperspb.String("username"), }, }, }}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Must provide private key", req: &pbs.CreateCredentialRequest{Item: &pb.Credential{ CredentialStoreId: store.GetPublicId(), Type: credential.SshPrivateKeySubtype.String(), Attrs: &pb.Credential_SshPrivateKeyAttributes{ SshPrivateKeyAttributes: &pb.SshPrivateKeyAttributes{ Username: wrapperspb.String("username"), }, }, }}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Must provide json secret", req: &pbs.CreateCredentialRequest{Item: &pb.Credential{ CredentialStoreId: store.GetPublicId(), Type: credential.JsonSubtype.String(), Attrs: &pb.Credential_JsonAttributes{ JsonAttributes: &pb.JsonAttributes{}, }, }}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "valid-up", req: &pbs.CreateCredentialRequest{Item: &pb.Credential{ CredentialStoreId: store.GetPublicId(), Type: credential.UsernamePasswordSubtype.String(), Attrs: &pb.Credential_UsernamePasswordAttributes{ UsernamePasswordAttributes: &pb.UsernamePasswordAttributes{ Username: wrapperspb.String("username"), Password: wrapperspb.String("password"), }, }, }}, idPrefix: globals.UsernamePasswordCredentialPrefix + "_", res: &pbs.CreateCredentialResponse{ Uri: fmt.Sprintf("credentials/%s_", globals.UsernamePasswordCredentialPrefix), Item: &pb.Credential{ Id: store.GetPublicId(), CredentialStoreId: store.GetPublicId(), CreatedTime: store.GetCreateTime().GetTimestamp(), UpdatedTime: store.GetUpdateTime().GetTimestamp(), Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, Version: 1, Type: credential.UsernamePasswordSubtype.String(), AuthorizedActions: testAuthorizedActions, }, }, }, { name: "valid-spk", req: &pbs.CreateCredentialRequest{Item: &pb.Credential{ CredentialStoreId: store.GetPublicId(), Type: credential.SshPrivateKeySubtype.String(), Attrs: &pb.Credential_SshPrivateKeyAttributes{ SshPrivateKeyAttributes: &pb.SshPrivateKeyAttributes{ Username: wrapperspb.String("username"), PrivateKey: wrapperspb.String(static.TestSshPrivateKeyPem), }, }, }}, idPrefix: globals.SshPrivateKeyCredentialPrefix + "_", res: &pbs.CreateCredentialResponse{ Uri: fmt.Sprintf("credentials/%s_", globals.SshPrivateKeyCredentialPrefix), Item: &pb.Credential{ Id: store.GetPublicId(), CredentialStoreId: store.GetPublicId(), CreatedTime: store.GetCreateTime().GetTimestamp(), UpdatedTime: store.GetUpdateTime().GetTimestamp(), Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, Version: 1, Type: credential.SshPrivateKeySubtype.String(), AuthorizedActions: testAuthorizedActions, }, }, }, { name: "valid-spk-with-passphrase", req: &pbs.CreateCredentialRequest{Item: &pb.Credential{ CredentialStoreId: store.GetPublicId(), Type: credential.SshPrivateKeySubtype.String(), Attrs: &pb.Credential_SshPrivateKeyAttributes{ SshPrivateKeyAttributes: &pb.SshPrivateKeyAttributes{ Username: wrapperspb.String("username"), PrivateKey: wrapperspb.String(string(testdata.PEMEncryptedKeys[0].PEMBytes)), PrivateKeyPassphrase: wrapperspb.String(testdata.PEMEncryptedKeys[0].EncryptionKey), }, }, }}, idPrefix: globals.SshPrivateKeyCredentialPrefix + "_", res: &pbs.CreateCredentialResponse{ Uri: fmt.Sprintf("credentials/%s_", globals.SshPrivateKeyCredentialPrefix), Item: &pb.Credential{ Id: store.GetPublicId(), CredentialStoreId: store.GetPublicId(), CreatedTime: store.GetCreateTime().GetTimestamp(), UpdatedTime: store.GetUpdateTime().GetTimestamp(), Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, Version: 1, Type: credential.SshPrivateKeySubtype.String(), AuthorizedActions: testAuthorizedActions, }, }, }, { name: "valid-json", req: &pbs.CreateCredentialRequest{Item: &pb.Credential{ CredentialStoreId: store.GetPublicId(), Type: credential.JsonSubtype.String(), Attrs: &pb.Credential_JsonAttributes{ JsonAttributes: &pb.JsonAttributes{ Object: obj.Struct, }, }, }}, idPrefix: globals.JsonCredentialPrefix + "_", res: &pbs.CreateCredentialResponse{ Uri: fmt.Sprintf("credentials/%s_", globals.JsonCredentialPrefix), Item: &pb.Credential{ Id: store.GetPublicId(), CredentialStoreId: store.GetPublicId(), CreatedTime: store.GetCreateTime().GetTimestamp(), UpdatedTime: store.GetUpdateTime().GetTimestamp(), Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, Version: 1, Type: credential.JsonSubtype.String(), AuthorizedActions: testAuthorizedActions, }, }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) s, err := NewService(ctx, iamRepoFn, repoFn, 1000) require.NoError(err, "Error when getting new credential store service.") got, gErr := s.CreateCredential(auth.DisabledAuthTestContext(iamRepoFn, prj.GetPublicId()), tc.req) if tc.wantErr || tc.err != nil { require.Error(gErr) if tc.err != nil { assert.True(errors.Is(gErr, tc.err), "CreateCredential(...) got error %v, wanted %v", gErr, tc.err) } return } require.NoError(gErr) if tc.res == nil { require.Nil(got) } if got != nil { assert.Contains(got.GetUri(), tc.res.Uri) assert.True(strings.HasPrefix(got.GetItem().GetId(), tc.idPrefix)) gotCreateTime := got.GetItem().GetCreatedTime() gotUpdateTime := got.GetItem().GetUpdatedTime() // Verify it is a credential store created after the test setup's default credential store assert.True(gotCreateTime.AsTime().After(store.CreateTime.AsTime()), "New credential should have been created after default credential store. Was created %v, which is after %v", gotCreateTime, store.CreateTime.AsTime()) assert.True(gotUpdateTime.AsTime().After(store.CreateTime.AsTime()), "New credential should have been updated after default credential store. Was updated %v, which is after %v", gotUpdateTime, store.CreateTime.AsTime()) // Calculate hmac databaseWrapper, err := kkms.GetWrapper(ctx, prj.PublicId, kms.KeyPurposeDatabase) require.NoError(err) switch tc.req.Item.Type { case credential.UsernamePasswordSubtype.String(): password := tc.req.GetItem().GetUsernamePasswordAttributes().GetPassword().GetValue() hm, err := crypto.HmacSha256(ctx, []byte(password), databaseWrapper, []byte(store.GetPublicId()), nil, crypto.WithEd25519()) require.NoError(err) // Validate attributes equal assert.Equal(tc.req.GetItem().GetUsernamePasswordAttributes().GetUsername().GetValue(), got.GetItem().GetUsernamePasswordAttributes().GetUsername().GetValue()) assert.Equal(base64.RawURLEncoding.EncodeToString([]byte(hm)), got.GetItem().GetUsernamePasswordAttributes().GetPasswordHmac()) assert.Empty(got.GetItem().GetUsernamePasswordAttributes().GetPassword()) case credential.SshPrivateKeySubtype.String(): pk := tc.req.GetItem().GetSshPrivateKeyAttributes().GetPrivateKey().GetValue() hm, err := crypto.HmacSha256(ctx, []byte(pk), databaseWrapper, []byte(store.GetPublicId()), nil) require.NoError(err) // Validate attributes equal assert.Equal(tc.req.GetItem().GetSshPrivateKeyAttributes().GetUsername().GetValue(), got.GetItem().GetSshPrivateKeyAttributes().GetUsername().GetValue()) assert.Equal(base64.RawURLEncoding.EncodeToString([]byte(hm)), got.GetItem().GetSshPrivateKeyAttributes().GetPrivateKeyHmac()) assert.Empty(got.GetItem().GetSshPrivateKeyAttributes().GetPrivateKey()) if pass := tc.req.GetItem().GetSshPrivateKeyAttributes().GetPrivateKeyPassphrase().GetValue(); pass != "" { hm, err := crypto.HmacSha256(ctx, []byte(pass), databaseWrapper, []byte(store.GetPublicId()), nil) require.NoError(err) assert.Equal(base64.RawURLEncoding.EncodeToString([]byte(hm)), got.GetItem().GetSshPrivateKeyAttributes().GetPrivateKeyPassphraseHmac()) assert.Empty(got.GetItem().GetSshPrivateKeyAttributes().GetPrivateKeyPassphrase()) } case credential.JsonSubtype.String(): hm, err := crypto.HmacSha256(ctx, objBytes, databaseWrapper, []byte(store.GetPublicId()), nil) require.NoError(err) // Validate attributes equal assert.Equal(base64.RawURLEncoding.EncodeToString([]byte(hm)), got.GetItem().GetJsonAttributes().GetObjectHmac()) assert.Empty(got.GetItem().GetJsonAttributes().GetObject()) default: require.Fail("unknown type") } // Clear attributes for compare below got.Item.Attrs = nil // Clear all values which are hard to compare against. got.Uri, tc.res.Uri = "", "" got.Item.Id, tc.res.Item.Id = "", "" got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil } assert.Empty(cmp.Diff( got, tc.res, protocmp.Transform(), protocmp.SortRepeatedFields(got), cmpopts.SortSlices(func(a, b string) bool { return a < b }), ), "CreateCredential(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } const TestSecondarySshPrivateKeyPem = ` -----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW QyNTUxOQAAACDxfwhEAZKrnsbQxOjVA3PFiB3bW3tSpNKx8TdMiCqlzQAAAJDmpbfr5qW3 6wAAAAtzc2gtZWQyNTUxOQAAACDxfwhEAZKrnsbQxOjVA3PFiB3bW3tSpNKx8TdMiCqlzQ AAAEBvvkQkH06ad2GpX1VVARzu9NkHA6gzamAaQ/hkn5FuZvF/CEQBkquextDE6NUDc8WI Hdtbe1Kk0rHxN0yIKqXNAAAACWplZmZAYXJjaAECAwQ= -----END OPENSSH PRIVATE KEY----- ` func TestUpdate(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") wrapper := db.TestWrapper(t) kkms := kms.TestKms(t, conn, wrapper) rw := db.New(conn) iamRepo := iam.TestRepo(t, conn, wrapper) iamRepoFn := func() (*iam.Repository, error) { return iamRepo, nil } staticRepoFn := func() (*static.Repository, error) { return static.NewRepository(context.Background(), rw, rw, kkms) } _, prj := iam.TestScopes(t, iamRepo) ctx := auth.DisabledAuthTestContext(iamRepoFn, prj.GetPublicId()) s, err := NewService(ctx, iamRepoFn, staticRepoFn, 1000) require.NoError(t, err) fieldmask := func(paths ...string) *fieldmaskpb.FieldMask { return &fieldmaskpb.FieldMask{Paths: paths} } store := static.TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) freshCredUp := func(user, pass string) (*static.UsernamePasswordCredential, func()) { t.Helper() cred := static.TestUsernamePasswordCredential(t, conn, wrapper, user, pass, store.GetPublicId(), prj.GetPublicId()) clean := func() { _, err := s.DeleteCredential(ctx, &pbs.DeleteCredentialRequest{Id: cred.GetPublicId()}) require.NoError(t, err) } return cred, clean } freshCredSpk := func(user string) (*static.SshPrivateKeyCredential, func()) { t.Helper() cred := static.TestSshPrivateKeyCredential(t, conn, wrapper, user, static.TestSshPrivateKeyPem, store.GetPublicId(), prj.GetPublicId()) clean := func() { _, err := s.DeleteCredential(ctx, &pbs.DeleteCredentialRequest{Id: cred.GetPublicId()}) require.NoError(t, err) } return cred, clean } freshCredJson := func() (*static.JsonCredential, func()) { t.Helper() obj, _ := static.TestJsonObject(t) cred := static.TestJsonCredential(t, conn, wrapper, store.GetPublicId(), prj.GetPublicId(), obj) clean := func() { _, err := s.DeleteCredential(ctx, &pbs.DeleteCredentialRequest{Id: cred.GetPublicId()}) require.NoError(t, err) } return cred, clean } secondSecret := &structpb.Struct{ Fields: map[string]*structpb.Value{ "username": structpb.NewStringValue("new-user"), "password": structpb.NewStringValue("new-password"), "hash": structpb.NewStringValue("0123456789"), }, } secondSecretBytes, err := json.Marshal(secondSecret) require.NoError(t, err) databaseWrapper, err := kkms.GetWrapper(context.Background(), prj.GetPublicId(), kms.KeyPurposeDatabase) require.NoError(t, err) successFailCases := []struct { name string req *pbs.UpdateCredentialRequest res func(cred *pb.Credential) *pb.Credential expErrorContains string }{ { name: "name", req: &pbs.UpdateCredentialRequest{ UpdateMask: fieldmask("name"), Item: &pb.Credential{ Name: wrapperspb.String("new-name"), }, }, res: func(in *pb.Credential) *pb.Credential { out := proto.Clone(in).(*pb.Credential) out.Name = wrapperspb.String("new-name") return out }, }, { name: "description", req: &pbs.UpdateCredentialRequest{ UpdateMask: fieldmask("description"), Item: &pb.Credential{ Description: wrapperspb.String("new-description"), }, }, res: func(in *pb.Credential) *pb.Credential { out := proto.Clone(in).(*pb.Credential) out.Description = wrapperspb.String("new-description") return out }, }, { name: "name-and-description", req: &pbs.UpdateCredentialRequest{ UpdateMask: fieldmask("name", "description"), Item: &pb.Credential{ Name: wrapperspb.String("new-name"), Description: wrapperspb.String("new-description"), }, }, res: func(in *pb.Credential) *pb.Credential { out := proto.Clone(in).(*pb.Credential) out.Name = wrapperspb.String("new-name") out.Description = wrapperspb.String("new-description") return out }, }, { name: "name-and-description-spk", req: &pbs.UpdateCredentialRequest{ UpdateMask: fieldmask("name", "description"), Item: &pb.Credential{ Name: wrapperspb.String("new-name"), Description: wrapperspb.String("new-description"), }, }, res: func(in *pb.Credential) *pb.Credential { out := proto.Clone(in).(*pb.Credential) out.Name = wrapperspb.String("new-name") out.Description = wrapperspb.String("new-description") return out }, }, { name: "update-username-up", req: &pbs.UpdateCredentialRequest{ UpdateMask: fieldmask("attributes.username"), Item: &pb.Credential{ Attrs: &pb.Credential_UsernamePasswordAttributes{ UsernamePasswordAttributes: &pb.UsernamePasswordAttributes{ Username: wrapperspb.String("new-user-name"), }, }, }, }, res: func(in *pb.Credential) *pb.Credential { out := proto.Clone(in).(*pb.Credential) out.GetUsernamePasswordAttributes().Username = wrapperspb.String("new-user-name") return out }, }, { name: "update-username-spk", req: &pbs.UpdateCredentialRequest{ UpdateMask: fieldmask("attributes.username"), Item: &pb.Credential{ Attrs: &pb.Credential_SshPrivateKeyAttributes{ SshPrivateKeyAttributes: &pb.SshPrivateKeyAttributes{ Username: wrapperspb.String("new-user-name"), }, }, }, }, res: func(in *pb.Credential) *pb.Credential { out := proto.Clone(in).(*pb.Credential) out.GetSshPrivateKeyAttributes().Username = wrapperspb.String("new-user-name") return out }, }, { name: "update-password", req: &pbs.UpdateCredentialRequest{ UpdateMask: fieldmask("attributes.password"), Item: &pb.Credential{ Attrs: &pb.Credential_UsernamePasswordAttributes{ UsernamePasswordAttributes: &pb.UsernamePasswordAttributes{ Password: wrapperspb.String("new-password"), }, }, }, }, res: func(in *pb.Credential) *pb.Credential { out := proto.Clone(in).(*pb.Credential) hm, err := crypto.HmacSha256(context.Background(), []byte("new-password"), databaseWrapper, []byte(store.GetPublicId()), nil, crypto.WithEd25519()) require.NoError(t, err) out.GetUsernamePasswordAttributes().PasswordHmac = base64.RawURLEncoding.EncodeToString([]byte(hm)) return out }, }, { name: "update-spk", req: &pbs.UpdateCredentialRequest{ UpdateMask: fieldmask("attributes.private_key"), Item: &pb.Credential{ Attrs: &pb.Credential_SshPrivateKeyAttributes{ SshPrivateKeyAttributes: &pb.SshPrivateKeyAttributes{ PrivateKey: wrapperspb.String(TestSecondarySshPrivateKeyPem), }, }, }, }, res: func(in *pb.Credential) *pb.Credential { out := proto.Clone(in).(*pb.Credential) hm, err := crypto.HmacSha256(context.Background(), []byte(TestSecondarySshPrivateKeyPem), databaseWrapper, []byte(store.GetPublicId()), nil) require.NoError(t, err) out.GetSshPrivateKeyAttributes().PrivateKeyHmac = base64.RawURLEncoding.EncodeToString([]byte(hm)) return out }, }, { name: "update-username-and-password", req: &pbs.UpdateCredentialRequest{ UpdateMask: fieldmask("attributes.username", "attributes.password"), Item: &pb.Credential{ Attrs: &pb.Credential_UsernamePasswordAttributes{ UsernamePasswordAttributes: &pb.UsernamePasswordAttributes{ Username: wrapperspb.String("new-username"), Password: wrapperspb.String("new-password"), }, }, }, }, res: func(in *pb.Credential) *pb.Credential { out := proto.Clone(in).(*pb.Credential) out.GetUsernamePasswordAttributes().Username = wrapperspb.String("new-username") hm, err := crypto.HmacSha256(context.Background(), []byte("new-password"), databaseWrapper, []byte(store.GetPublicId()), nil, crypto.WithEd25519()) require.NoError(t, err) out.GetUsernamePasswordAttributes().PasswordHmac = base64.RawURLEncoding.EncodeToString([]byte(hm)) return out }, }, { name: "update-username-and-spk", req: &pbs.UpdateCredentialRequest{ UpdateMask: fieldmask("attributes.username", "attributes.private_key"), Item: &pb.Credential{ Attrs: &pb.Credential_SshPrivateKeyAttributes{ SshPrivateKeyAttributes: &pb.SshPrivateKeyAttributes{ Username: wrapperspb.String("new-username"), PrivateKey: wrapperspb.String(TestSecondarySshPrivateKeyPem), }, }, }, }, res: func(in *pb.Credential) *pb.Credential { out := proto.Clone(in).(*pb.Credential) out.GetSshPrivateKeyAttributes().Username = wrapperspb.String("new-username") hm, err := crypto.HmacSha256(context.Background(), []byte(TestSecondarySshPrivateKeyPem), databaseWrapper, []byte(store.GetPublicId()), nil) require.NoError(t, err) out.GetSshPrivateKeyAttributes().PrivateKeyHmac = base64.RawURLEncoding.EncodeToString([]byte(hm)) return out }, }, { name: "update-spk-with-passphrase", req: &pbs.UpdateCredentialRequest{ UpdateMask: fieldmask("attributes.private_key", "attributes.private_key_passphrase"), Item: &pb.Credential{ Attrs: &pb.Credential_SshPrivateKeyAttributes{ SshPrivateKeyAttributes: &pb.SshPrivateKeyAttributes{ PrivateKey: wrapperspb.String(string(testdata.PEMEncryptedKeys[0].PEMBytes)), PrivateKeyPassphrase: wrapperspb.String(testdata.PEMEncryptedKeys[0].EncryptionKey), }, }, }, }, res: func(in *pb.Credential) *pb.Credential { out := proto.Clone(in).(*pb.Credential) hm, err := crypto.HmacSha256(context.Background(), testdata.PEMEncryptedKeys[0].PEMBytes, databaseWrapper, []byte(store.GetPublicId()), nil) require.NoError(t, err) out.GetSshPrivateKeyAttributes().PrivateKeyHmac = base64.RawURLEncoding.EncodeToString([]byte(hm)) hm, err = crypto.HmacSha256(context.Background(), []byte(testdata.PEMEncryptedKeys[0].EncryptionKey), databaseWrapper, []byte(store.GetPublicId()), nil) require.NoError(t, err) out.GetSshPrivateKeyAttributes().PrivateKeyPassphraseHmac = base64.RawURLEncoding.EncodeToString([]byte(hm)) return out }, }, { name: "update-json", req: &pbs.UpdateCredentialRequest{ UpdateMask: fieldmask("attributes.object.username", "attributes.object.password", "attributes.object.hash"), Item: &pb.Credential{ Attrs: &pb.Credential_JsonAttributes{ JsonAttributes: &pb.JsonAttributes{ Object: secondSecret, }, }, }, }, res: func(in *pb.Credential) *pb.Credential { out := proto.Clone(in).(*pb.Credential) hm, err := crypto.HmacSha256(context.Background(), secondSecretBytes, databaseWrapper, []byte(store.GetPublicId()), nil) require.NoError(t, err) out.GetJsonAttributes().ObjectHmac = base64.RawURLEncoding.EncodeToString([]byte(hm)) return out }, }, { name: "update-empty-object-json", req: &pbs.UpdateCredentialRequest{ UpdateMask: fieldmask("attributes.object.password"), Item: &pb.Credential{ Attrs: &pb.Credential_JsonAttributes{ JsonAttributes: &pb.JsonAttributes{ Object: &structpb.Struct{}, }, }, }, }, expErrorContains: "This is a required field and cannot be set to empty", }, { name: "update-nil-object-json", req: &pbs.UpdateCredentialRequest{ UpdateMask: fieldmask("attributes.object.password"), Item: &pb.Credential{ Attrs: &pb.Credential_JsonAttributes{ JsonAttributes: &pb.JsonAttributes{}, }, }, }, expErrorContains: "This is a required field and cannot be set to empty", }, { name: "update-spk-with-bad-passphrase", req: &pbs.UpdateCredentialRequest{ UpdateMask: fieldmask("attributes.private_key", "attributes.private_key_passphrase"), Item: &pb.Credential{ Attrs: &pb.Credential_SshPrivateKeyAttributes{ SshPrivateKeyAttributes: &pb.SshPrivateKeyAttributes{ PrivateKey: wrapperspb.String(string(testdata.PEMEncryptedKeys[0].PEMBytes)), PrivateKeyPassphrase: wrapperspb.String(strings.ToLower(testdata.PEMEncryptedKeys[0].EncryptionKey)), }, }, }, }, expErrorContains: "Incorrect private key passphrase", }, { name: "update-non-passphrase-spk-with-passphrase", req: &pbs.UpdateCredentialRequest{ UpdateMask: fieldmask("attributes.private_key"), Item: &pb.Credential{ Attrs: &pb.Credential_SshPrivateKeyAttributes{ SshPrivateKeyAttributes: &pb.SshPrivateKeyAttributes{ PrivateKey: wrapperspb.String(static.TestSshPrivateKeyPem), PrivateKeyPassphrase: wrapperspb.String(testdata.PEMEncryptedKeys[0].EncryptionKey), }, }, }, }, expErrorContains: "Passphrase supplied for unencrypted key", }, { name: "update-non-passphrase-spk-with-no-passphrase", req: &pbs.UpdateCredentialRequest{ UpdateMask: fieldmask("attributes.private_key"), Item: &pb.Credential{ Attrs: &pb.Credential_SshPrivateKeyAttributes{ SshPrivateKeyAttributes: &pb.SshPrivateKeyAttributes{ PrivateKey: wrapperspb.String(static.TestSshPrivateKeyPem), }, }, }, }, res: func(in *pb.Credential) *pb.Credential { out := proto.Clone(in).(*pb.Credential) hm, err := crypto.HmacSha256(context.Background(), []byte(static.TestSshPrivateKeyPem), databaseWrapper, []byte(store.GetPublicId()), nil) require.NoError(t, err) out.GetSshPrivateKeyAttributes().PrivateKeyHmac = base64.RawURLEncoding.EncodeToString([]byte(hm)) out.GetSshPrivateKeyAttributes().PrivateKeyPassphraseHmac = "" return out }, }, } for _, tc := range successFailCases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) var cred credential.Static var cleanup func() if strings.Contains(tc.name, "spk") { cred, cleanup = freshCredSpk("user") } else if strings.Contains(tc.name, "json") { cred, cleanup = freshCredJson() } else { cred, cleanup = freshCredUp("user", "pass") } defer cleanup() if tc.req.Item.GetVersion() == 0 { tc.req.Item.Version = 1 } if tc.req.GetId() == "" { tc.req.Id = cred.GetPublicId() } resToChange, err := s.GetCredential(ctx, &pbs.GetCredentialRequest{Id: cred.GetPublicId()}) require.NoError(err) got, gErr := s.UpdateCredential(ctx, tc.req) if tc.expErrorContains != "" { require.NotNil(gErr) assert.Contains(gErr.Error(), tc.expErrorContains) return } require.NoError(gErr) require.NotNil(got) want := &pbs.UpdateCredentialResponse{Item: tc.res(resToChange.GetItem())} gotUpdateTime := got.GetItem().GetUpdatedTime() created := cred.GetCreateTime().GetTimestamp() assert.True(gotUpdateTime.AsTime().After(created.AsTime()), "Should have been updated after it's creation. Was updated %v, which is after %v", gotUpdateTime, created) want.Item.UpdatedTime = got.Item.UpdatedTime assert.EqualValues(2, got.Item.Version) want.Item.Version = 2 assert.Empty(cmp.Diff( got, want, protocmp.Transform(), cmpopts.SortSlices(func(a, b string) bool { return a < b }), )) }) } // cant update read only fields credJson, cleanupJson := freshCredJson() defer cleanupJson() // cant update read only fields credUp, cleanUp := freshCredUp("user", "pass") defer cleanUp() newStore := static.TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) roCases := []struct { path string item *pb.Credential matcher func(t *testing.T, e error) // When not set defaults to checking against InvalidArgument Error }{ { path: "type", item: &pb.Credential{Type: "something"}, }, { path: "store_id", item: &pb.Credential{CredentialStoreId: newStore.GetPublicId()}, }, { path: "updated_time", item: &pb.Credential{UpdatedTime: timestamppb.Now()}, }, { path: "created_time", item: &pb.Credential{UpdatedTime: timestamppb.Now()}, }, { path: "authorized_actions", item: &pb.Credential{AuthorizedActions: append(testAuthorizedActions, "another")}, }, } for _, tc := range roCases { t.Run(fmt.Sprintf("ReadOnlyField/%s", tc.path), func(t *testing.T) { req := &pbs.UpdateCredentialRequest{ Id: credUp.GetPublicId(), Item: tc.item, UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{tc.path}}, } req.Item.Version = credUp.Version got, gErr := s.UpdateCredential(ctx, req) assert.Error(t, gErr) matcher := tc.matcher if matcher == nil { matcher = func(t *testing.T, e error) { assert.Truef(t, errors.Is(gErr, handlers.ApiErrorWithCode(codes.InvalidArgument)), "got error %v, wanted invalid argument", gErr) } } matcher(t, gErr) assert.Nil(t, got) req = &pbs.UpdateCredentialRequest{ Id: credJson.GetPublicId(), Item: tc.item, UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{tc.path}}, } req.Item.Version = credJson.Version got, gErr = s.UpdateCredential(ctx, req) assert.Error(t, gErr) matcher = tc.matcher if matcher == nil { matcher = func(t *testing.T, e error) { assert.Truef(t, errors.Is(gErr, handlers.ApiErrorWithCode(codes.InvalidArgument)), "got error %v, wanted invalid argument", gErr) } } matcher(t, gErr) assert.Nil(t, got) }) } } func TestListPagination(t *testing.T) { // Set database read timeout to avoid duplicates in response oldReadTimeout := globals.RefreshReadLookbackDuration globals.RefreshReadLookbackDuration = 0 t.Cleanup(func() { globals.RefreshReadLookbackDuration = oldReadTimeout }) assert, require := assert.New(t), require.New(t) ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") sqlDB, err := conn.SqlDB(ctx) require.NoError(err) wrapper := db.TestWrapper(t) kmsRepo := kms.TestKms(t, conn, wrapper) rw := db.New(conn) iamRepo := iam.TestRepo(t, conn, wrapper) iamRepoFn := func() (*iam.Repository, error) { return iamRepo, nil } staticRepoFn := func() (*static.Repository, error) { return static.NewRepository(ctx, rw, rw, kmsRepo) } tokenRepoFn := func() (*authtoken.Repository, error) { return authtoken.NewRepository(ctx, rw, rw, kmsRepo) } serversRepoFn := func() (*server.Repository, error) { return server.NewRepository(ctx, rw, rw, kmsRepo) } repo, err := staticRepoFn() require.NoError(err) tokenRepo, err := tokenRepoFn() require.NoError(err) _, prjNoStores := iam.TestScopes(t, iamRepo) o, prj := iam.TestScopes(t, iamRepo) credStore := static.TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) emptyStore := static.TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) databaseWrapper, err := kmsRepo.GetWrapper(ctx, prj.GetPublicId(), kms.KeyPurposeDatabase) require.NoError(err) var allCredentials []*pb.Credential testObj, testObjBytes := static.TestJsonObject(t) for _, l := range static.TestJsonCredentials(t, conn, wrapper, credStore.GetPublicId(), prj.PublicId, testObj, 5) { hm, err := crypto.HmacSha256(ctx, []byte(testObjBytes), databaseWrapper, []byte(credStore.GetPublicId()), nil) require.NoError(err) allCredentials = append(allCredentials, staticJsonCredentialToProto(l, prj, hm)) } for _, l := range static.TestSshPrivateKeyCredentials(t, conn, wrapper, "username", static.TestSshPrivateKeyPem, credStore.GetPublicId(), prj.PublicId, 5) { hm, err := crypto.HmacSha256(ctx, []byte(static.TestSshPrivateKeyPem), databaseWrapper, []byte(credStore.GetPublicId()), nil) require.NoError(err) allCredentials = append(allCredentials, staticSshCredentialToProto(l, prj, hm)) } for _, l := range static.TestUsernamePasswordCredentials(t, conn, wrapper, "username", "password", credStore.GetPublicId(), prj.PublicId, 5) { hm, err := crypto.HmacSha256(ctx, []byte("password"), databaseWrapper, []byte(credStore.GetPublicId()), nil, crypto.WithEd25519()) require.NoError(err) allCredentials = append(allCredentials, staticPasswordCredentialToProto(l, prj, hm)) } // Reverse slices since response is ordered by created_time descending (newest first) slices.Reverse(allCredentials) // Run analyze to update postgres meta tables _, err = sqlDB.ExecContext(ctx, "analyze") require.NoError(err) authMethod := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0] // auth account is only used to join auth method to user. // We don't do anything else with the auth account in the test setup. acct := password.TestAccount(t, conn, authMethod.GetPublicId(), "test_user") u := iam.TestUser(t, iamRepo, o.GetPublicId(), iam.WithAccountIds(acct.PublicId)) role1 := iam.TestRole(t, conn, prj.GetPublicId()) iam.TestRoleGrant(t, conn, role1.GetPublicId(), "ids=*;type=*;actions=*") iam.TestUserRole(t, conn, role1.GetPublicId(), u.GetPublicId()) role2 := iam.TestRole(t, conn, prjNoStores.GetPublicId()) iam.TestRoleGrant(t, conn, role2.GetPublicId(), "ids=*;type=*;actions=*") iam.TestUserRole(t, conn, role2.GetPublicId(), u.GetPublicId()) at, err := tokenRepo.CreateAuthToken(ctx, u, acct.GetPublicId()) require.NoError(err) // Test without anon user requestInfo := authpb.RequestInfo{ TokenFormat: uint32(auth.AuthTokenTypeBearer), PublicId: at.GetPublicId(), Token: at.GetToken(), } requestContext := context.WithValue(context.Background(), requests.ContextRequestInformationKey, &requests.RequestContext{}) ctx = auth.NewVerifierContext(requestContext, iamRepoFn, tokenRepoFn, serversRepoFn, kmsRepo, &requestInfo) s, err := NewService(ctx, iamRepoFn, staticRepoFn, 1000) require.NoError(err) // Start paginating, recursively req := &pbs.ListCredentialsRequest{ CredentialStoreId: credStore.PublicId, Filter: "", ListToken: "", PageSize: 2, } got, err := s.ListCredentials(ctx, req) require.NoError(err) require.Len(got.GetItems(), 2) // Compare without comparing the list token assert.Empty( cmp.Diff( got, &pbs.ListCredentialsResponse{ Items: allCredentials[0:2], ResponseType: "delta", ListToken: "", SortBy: "created_time", SortDir: "desc", RemovedIds: nil, EstItemCount: 15, }, cmpopts.SortSlices(func(a, b string) bool { return a < b }), protocmp.Transform(), protocmp.IgnoreFields(&pbs.ListCredentialsResponse{}, "list_token"), ), ) // Request second page req.ListToken = got.ListToken got, err = s.ListCredentials(ctx, req) require.NoError(err) require.Len(got.GetItems(), 2) // Compare without comparing the list token assert.Empty( cmp.Diff( got, &pbs.ListCredentialsResponse{ Items: allCredentials[2:4], ResponseType: "delta", ListToken: "", SortBy: "created_time", SortDir: "desc", RemovedIds: nil, EstItemCount: 15, }, cmpopts.SortSlices(func(a, b string) bool { return a < b }), protocmp.Transform(), protocmp.IgnoreFields(&pbs.ListCredentialsResponse{}, "list_token"), ), ) // Request rest of results req.ListToken = got.ListToken req.PageSize = 15 got, err = s.ListCredentials(ctx, req) require.NoError(err) require.Len(got.GetItems(), 11) // Compare without comparing the list token assert.Empty( cmp.Diff( got, &pbs.ListCredentialsResponse{ Items: allCredentials[4:], ResponseType: "complete", ListToken: "", SortBy: "created_time", SortDir: "desc", RemovedIds: nil, EstItemCount: 15, }, cmpopts.SortSlices(func(a, b string) bool { return a < b }), protocmp.Transform(), protocmp.IgnoreFields(&pbs.ListCredentialsResponse{}, "list_token"), ), ) // Create another credential newCred := static.TestJsonCredential(t, conn, wrapper, credStore.GetPublicId(), prj.GetPublicId(), testObj) hm, err := crypto.HmacSha256(ctx, []byte(testObjBytes), databaseWrapper, []byte(credStore.GetPublicId()), nil) require.NoError(err) pbNewCred := staticJsonCredentialToProto(newCred, prj, hm) // Add to the front since it's most recently updated allCredentials = append([]*pb.Credential{pbNewCred}, allCredentials...) // Delete one of the other credentials _, err = repo.DeleteCredential(ctx, prj.GetPublicId(), allCredentials[len(allCredentials)-1].GetId()) require.NoError(err) deletedCred := allCredentials[len(allCredentials)-1] allCredentials = allCredentials[:len(allCredentials)-1] // Update one of the other credentials allCredentials[1].Name = wrapperspb.String("new-name") allCredentials[1].Version = 2 updatedCredential := &static.UsernamePasswordCredential{ UsernamePasswordCredential: &store.UsernamePasswordCredential{ PublicId: allCredentials[1].GetId(), Name: allCredentials[1].GetName().GetValue(), StoreId: allCredentials[1].GetCredentialStoreId(), }, } cred, _, err := repo.UpdateUsernamePasswordCredential(ctx, prj.GetPublicId(), updatedCredential, 1, []string{"name"}) require.NoError(err) allCredentials[1].UpdatedTime = cred.UpdateTime.GetTimestamp() allCredentials[1].Version = cred.GetVersion() // Add to the front since it's most recently updated allCredentials = append( []*pb.Credential{allCredentials[1]}, append( []*pb.Credential{allCredentials[0]}, allCredentials[2:]..., )..., ) // Run analyze to update postgres meta tables _, err = sqlDB.ExecContext(ctx, "analyze") require.NoError(err) // Request updated results req.ListToken = got.ListToken req.PageSize = 1 got, err = s.ListCredentials(ctx, req) require.NoError(err) assert.Len(got.GetItems(), 1) // Compare without comparing the list token assert.Empty( cmp.Diff( got, &pbs.ListCredentialsResponse{ Items: []*pb.Credential{allCredentials[0]}, ResponseType: "delta", ListToken: "", SortBy: "updated_time", SortDir: "desc", // Should contain the deleted credential RemovedIds: []string{deletedCred.Id}, EstItemCount: 15, }, cmpopts.SortSlices(func(a, b string) bool { return a < b }), protocmp.Transform(), protocmp.IgnoreFields(&pbs.ListCredentialsResponse{}, "list_token"), ), ) // Get next page req.ListToken = got.ListToken got, err = s.ListCredentials(ctx, req) require.NoError(err) require.Len(got.GetItems(), 1) // Compare without comparing the list token assert.Empty( cmp.Diff( got, &pbs.ListCredentialsResponse{ Items: []*pb.Credential{allCredentials[1]}, ResponseType: "complete", SortBy: "updated_time", SortDir: "desc", RemovedIds: nil, EstItemCount: 15, }, cmpopts.SortSlices(func(a, b string) bool { return a < b }), protocmp.Transform(), protocmp.IgnoreFields(&pbs.ListCredentialsResponse{}, "list_token"), ), ) // Request new page with filter requiring looping // to fill the page. req.ListToken = "" req.PageSize = 1 req.Filter = fmt.Sprintf(`"/item/id"==%q or "/item/id"==%q`, allCredentials[len(allCredentials)-2].Id, allCredentials[len(allCredentials)-1].Id) got, err = s.ListCredentials(ctx, req) require.NoError(err) require.Len(got.GetItems(), 1) assert.Empty( cmp.Diff( got, &pbs.ListCredentialsResponse{ Items: []*pb.Credential{allCredentials[len(allCredentials)-2]}, ResponseType: "delta", ListToken: "", SortBy: "created_time", SortDir: "desc", // Should be empty again RemovedIds: nil, EstItemCount: 15, }, cmpopts.SortSlices(func(a, b string) bool { return a < b }), protocmp.Transform(), protocmp.IgnoreFields(&pbs.ListCredentialsResponse{}, "list_token"), ), ) req.ListToken = got.ListToken // Get the second page got, err = s.ListCredentials(ctx, req) require.NoError(err) require.Len(got.GetItems(), 1) assert.Empty( cmp.Diff( got, &pbs.ListCredentialsResponse{ Items: []*pb.Credential{allCredentials[len(allCredentials)-1]}, ResponseType: "complete", ListToken: "", SortBy: "created_time", SortDir: "desc", RemovedIds: nil, EstItemCount: 15, }, cmpopts.SortSlices(func(a, b string) bool { return a < b }), protocmp.Transform(), protocmp.IgnoreFields(&pbs.ListCredentialsResponse{}, "list_token"), ), ) req.ListToken = got.ListToken // List items in the empty store req = &pbs.ListCredentialsRequest{ CredentialStoreId: emptyStore.PublicId, Filter: "", ListToken: "", PageSize: 2, } got, err = s.ListCredentials(ctx, req) require.NoError(err) require.Len(got.GetItems(), 0) // Compare without comparing the list token assert.Empty( cmp.Diff( got, &pbs.ListCredentialsResponse{ Items: nil, ResponseType: "complete", SortBy: "created_time", SortDir: "desc", RemovedIds: nil, }, cmpopts.SortSlices(func(a, b string) bool { return a < b }), protocmp.Transform(), protocmp.IgnoreFields(&pbs.ListCredentialsResponse{}, "list_token"), ), ) // Create unauthenticated user unauthAt := authtoken.TestAuthToken(t, conn, kmsRepo, o.GetPublicId()) unauthR := iam.TestRole(t, conn, prj.GetPublicId()) _ = iam.TestUserRole(t, conn, unauthR.GetPublicId(), unauthAt.GetIamUserId()) // Make a request with the unauthenticated user, // ensure the response is 403 forbidden. requestInfo = authpb.RequestInfo{ TokenFormat: uint32(auth.AuthTokenTypeBearer), PublicId: unauthAt.GetPublicId(), Token: unauthAt.GetToken(), } requestContext = context.WithValue(context.Background(), requests.ContextRequestInformationKey, &requests.RequestContext{}) ctx = auth.NewVerifierContext(requestContext, iamRepoFn, tokenRepoFn, serversRepoFn, kmsRepo, &requestInfo) _, err = s.ListCredentials(ctx, &pbs.ListCredentialsRequest{ CredentialStoreId: credStore.PublicId, }) require.Error(err) assert.Equal(handlers.ForbiddenError(), err) }