// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package ldap import ( "context" "crypto/ed25519" "crypto/rand" "crypto/x509" "fmt" "sort" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/oplog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/testing/protocmp" ) func TestRepository_UpdateAuthMethod(t *testing.T) { t.Parallel() testCtx := context.Background() testConn, _ := db.TestSetup(t, "postgres") testWrapper := db.TestWrapper(t) testKms := kms.TestKms(t, testConn, testWrapper) testRw := db.New(testConn) testRepo, err := NewRepository(testCtx, testRw, testRw, testKms) require.NoError(t, err) org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper)) databaseWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) require.NoError(t, err) testCert, testCertEncoded := TestGenerateCA(t, "localhost") _, testPrivKey, err := ed25519.GenerateKey(rand.Reader) require.NoError(t, err) derPrivKey, err := x509.MarshalPKCS8PrivateKey(testPrivKey) require.NoError(t, err) _, testCertEncoded2 := TestGenerateCA(t, "localhost") _, testPrivKey2, err := ed25519.GenerateKey(rand.Reader) require.NoError(t, err) derPrivKey2, err := x509.MarshalPKCS8PrivateKey(testPrivKey2) require.NoError(t, err) tests := []struct { name string ctx context.Context repo *Repository setup func() *AuthMethod updateWith func(orig *AuthMethod) *AuthMethod fieldMasks []string version uint32 opt []Option want func(orig, updateWith *AuthMethod) *AuthMethod wantErrMatch *errors.Template wantErrContains string wantNoRowsUpdated bool }{ { name: "update-everything", ctx: testCtx, repo: testRepo, setup: func() *AuthMethod { return TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1", "ldap://ldap2"}, WithName(testCtx, "update-everything-test-name"), WithDescription(testCtx, "update-everything-test-description"), WithUpnDomain(testCtx, "orig.alice.com"), WithUserDn(testCtx, "orig-user-dn"), WithUserAttr(testCtx, "orig-user-attr"), WithUserFilter(testCtx, "orig-user-filter"), WithGroupDn(testCtx, "orig-group-dn"), WithGroupAttr(testCtx, "orig-group-attr"), WithGroupFilter(testCtx, "orig-group-filter"), WithBindCredential(testCtx, "orig-bind-dn", "orig-bind-password"), WithCertificates(testCtx, testCert), WithClientCertificate(testCtx, derPrivKey, testCert), // not a client cert but good enough for this test. WithAccountAttributeMap(testCtx, map[string]AccountToAttribute{ "displayName": ToFullNameAttribute, "mail": ToEmailAttribute, }), WithDerefAliases(testCtx, DerefAlways), WithMaximumPageSize(testCtx, 10), ) }, updateWith: func(orig *AuthMethod) *AuthMethod { am := AllocAuthMethod() am.PublicId = orig.PublicId am.Urls = []string{"ldaps://ldap1.alice.com", "ldaps://ldap2.alice.com"} am.OperationalState = string(ActivePublicState) am.Name = "update-everything-updated-name" am.Description = "update-everything-updated-description" am.StartTls = true am.InsecureTls = true am.DiscoverDn = true am.AnonGroupSearch = true am.EnableGroups = true am.UseTokenGroups = true am.UpnDomain = "alice.com" am.UserDn = "user-dn" am.UserAttr = "user-attr" am.UserFilter = "user-filter" am.GroupDn = "group-dn" am.GroupAttr = "group-attr" am.GroupFilter = "group-filter" am.BindDn = "bind-dn" am.BindPassword = "bind-password" am.Certificates = []string{testCertEncoded2} am.ClientCertificate = testCertEncoded2 am.ClientCertificateKey = derPrivKey2 am.AccountAttributeMaps = []string{ fmt.Sprintf("%s=%s", "cn", ToFullNameAttribute), } am.DereferenceAliases = string(NeverDerefAliases) am.MaximumPageSize = 100 return &am }, fieldMasks: []string{ OperationalStateField, NameField, DescriptionField, UrlsField, StartTlsField, InsecureTlsField, DiscoverDnField, AnonGroupSearchField, UpnDomainField, UserDnField, UserAttrField, UserFilterField, EnableGroupsField, UseTokenGroupsField, GroupDnField, GroupAttrField, GroupFilterField, BindDnField, BindPasswordField, CertificatesField, ClientCertificateField, ClientCertificateKeyField, AccountAttributeMapsField, DerefAliasesField, MaximumPageSizeField, }, version: 1, want: func(orig, updateWith *AuthMethod) *AuthMethod { am := orig.clone() am.OperationalState = string(ActivePublicState) am.Name = updateWith.Name am.Description = updateWith.Description am.Urls = updateWith.Urls am.StartTls = updateWith.StartTls am.InsecureTls = updateWith.InsecureTls am.DiscoverDn = updateWith.DiscoverDn am.AnonGroupSearch = updateWith.AnonGroupSearch am.UpnDomain = updateWith.UpnDomain am.UserDn = updateWith.UserDn am.UserAttr = updateWith.UserAttr am.UserFilter = updateWith.UserFilter am.EnableGroups = updateWith.EnableGroups am.UseTokenGroups = updateWith.UseTokenGroups am.GroupDn = updateWith.GroupDn am.GroupAttr = updateWith.GroupAttr am.GroupFilter = updateWith.GroupFilter am.BindDn = updateWith.BindDn am.BindPassword = updateWith.BindPassword am.BindPasswordHmac = updateWith.BindPasswordHmac am.Certificates = updateWith.Certificates am.ClientCertificateKey = updateWith.ClientCertificateKey am.ClientCertificate = updateWith.ClientCertificate am.ClientCertificateKeyHmac = updateWith.ClientCertificateKeyHmac am.AccountAttributeMaps = updateWith.AccountAttributeMaps am.DereferenceAliases = string(NeverDerefAliases) am.MaximumPageSize = 100 return am }, }, { name: "update-nothing", ctx: testCtx, repo: testRepo, setup: func() *AuthMethod { return TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1", "ldap://ldap2"}, WithName(testCtx, "update-nothing-test-name"), WithDescription(testCtx, "update-nothing-test-description"), WithUpnDomain(testCtx, "orig.alice.com"), WithUserDn(testCtx, "orig-user-dn"), WithUserAttr(testCtx, "orig-user-attr"), WithUserFilter(testCtx, "orig-user-filter"), WithGroupDn(testCtx, "orig-group-dn"), WithGroupAttr(testCtx, "orig-group-attr"), WithGroupFilter(testCtx, "orig-group-filter"), WithBindCredential(testCtx, "orig-bind-dn", "orig-bind-password"), WithCertificates(testCtx, testCert), WithClientCertificate(testCtx, derPrivKey, testCert), // not a client cert but good enough for this test. WithAccountAttributeMap(testCtx, map[string]AccountToAttribute{ "mail": ToEmailAttribute, "cn": ToFullNameAttribute, }), ) }, updateWith: func(orig *AuthMethod) *AuthMethod { return orig.clone() }, fieldMasks: []string{ NameField, DescriptionField, UrlsField, StartTlsField, InsecureTlsField, DiscoverDnField, AnonGroupSearchField, UpnDomainField, UserDnField, UserAttrField, UserFilterField, EnableGroupsField, UseTokenGroupsField, GroupDnField, GroupAttrField, GroupFilterField, BindDnField, BindPasswordField, CertificatesField, ClientCertificateField, AccountAttributeMapsField, }, version: 1, want: func(orig, updateWith *AuthMethod) *AuthMethod { return orig.clone() }, }, { name: "only-update-attributes", ctx: testCtx, repo: testRepo, setup: func() *AuthMethod { return TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1", "ldap://ldap2"}, WithName(testCtx, "only-update-attributes-test-name"), WithDescription(testCtx, "only-update-attributes-test-description"), WithUpnDomain(testCtx, "orig.alice.com"), WithUserDn(testCtx, "orig-user-dn"), WithUserAttr(testCtx, "orig-user-attr"), WithUserFilter(testCtx, "orig-user-filter"), WithGroupDn(testCtx, "orig-group-dn"), WithGroupAttr(testCtx, "orig-group-attr"), WithGroupFilter(testCtx, "orig-group-filter"), WithBindCredential(testCtx, "orig-bind-dn", "orig-bind-password"), WithCertificates(testCtx, testCert), WithClientCertificate(testCtx, derPrivKey, testCert), // not a client cert but good enough for this test. ) }, updateWith: func(orig *AuthMethod) *AuthMethod { am := AllocAuthMethod() am.PublicId = orig.PublicId am.OperationalState = string(ActivePublicState) am.Name = "only-update-attributes-updated-name" am.Description = "only-update-attributes-updated-description" am.StartTls = true am.InsecureTls = true am.DiscoverDn = true am.AnonGroupSearch = true am.EnableGroups = true am.UseTokenGroups = true am.UpnDomain = "alice.com" return &am }, fieldMasks: []string{ NameField, DescriptionField, StartTlsField, InsecureTlsField, DiscoverDnField, AnonGroupSearchField, EnableGroupsField, UseTokenGroupsField, UpnDomainField, }, version: 1, want: func(orig, updateWith *AuthMethod) *AuthMethod { am := orig.clone() am.Name = updateWith.Name am.Description = updateWith.Description am.StartTls = updateWith.StartTls am.InsecureTls = updateWith.InsecureTls am.DiscoverDn = updateWith.DiscoverDn am.AnonGroupSearch = updateWith.AnonGroupSearch am.UpnDomain = updateWith.UpnDomain am.EnableGroups = updateWith.EnableGroups am.UseTokenGroups = updateWith.UseTokenGroups return am }, }, { name: "all-attributes-set-to-null-or-empty", ctx: testCtx, repo: testRepo, setup: func() *AuthMethod { return TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1", "ldap://ldap2"}, WithName(testCtx, "all-attributes-set-to-null-or-empty-test-name"), WithDescription(testCtx, "all-attributes-set-to-null-or-empty-description"), WithUpnDomain(testCtx, "orig.alice.com"), WithUserDn(testCtx, "orig-user-dn"), WithUserAttr(testCtx, "orig-user-attr"), WithUserFilter(testCtx, "orig-user-filter"), WithGroupDn(testCtx, "orig-group-dn"), WithGroupAttr(testCtx, "orig-group-attr"), WithGroupFilter(testCtx, "orig-group-filter"), WithBindCredential(testCtx, "orig-bind-dn", "orig-bind-password"), WithCertificates(testCtx, testCert), WithClientCertificate(testCtx, derPrivKey, testCert), // not a client cert but good enough for this test. WithDerefAliases(testCtx, DerefAlways), WithMaximumPageSize(testCtx, 10), ) }, updateWith: func(orig *AuthMethod) *AuthMethod { am := AllocAuthMethod() am.PublicId = orig.PublicId return &am }, fieldMasks: []string{ NameField, DescriptionField, StartTlsField, InsecureTlsField, DiscoverDnField, AnonGroupSearchField, UpnDomainField, EnableGroupsField, UseTokenGroupsField, DerefAliasesField, MaximumPageSizeField, }, version: 1, want: func(orig, updateWith *AuthMethod) *AuthMethod { am := orig.clone() am.Name = updateWith.Name am.Description = updateWith.Description am.StartTls = updateWith.StartTls am.InsecureTls = updateWith.InsecureTls am.DiscoverDn = updateWith.DiscoverDn am.AnonGroupSearch = updateWith.AnonGroupSearch am.UpnDomain = updateWith.UpnDomain am.EnableGroups = updateWith.EnableGroups am.UseTokenGroups = updateWith.UseTokenGroups am.MaximumPageSize = updateWith.MaximumPageSize am.DereferenceAliases = updateWith.DereferenceAliases return am }, }, { name: "only-update-value-objects", ctx: testCtx, repo: testRepo, setup: func() *AuthMethod { return TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1", "ldap://ldap2"}, WithName(testCtx, "only-update-value-objects-test-name"), WithDescription(testCtx, "orig-test-description"), WithUpnDomain(testCtx, "orig.alice.com"), WithUserDn(testCtx, "orig-user-dn"), WithUserAttr(testCtx, "orig-user-attr"), WithUserFilter(testCtx, "orig-user-filter"), WithGroupDn(testCtx, "orig-group-dn"), WithGroupAttr(testCtx, "orig-group-attr"), WithGroupFilter(testCtx, "orig-group-filter"), WithBindCredential(testCtx, "orig-bind-dn", "orig-bind-password"), WithCertificates(testCtx, testCert), WithClientCertificate(testCtx, derPrivKey, testCert), // not a client cert but good enough for this test. WithAccountAttributeMap(testCtx, map[string]AccountToAttribute{ "mail": ToEmailAttribute, "cn": ToFullNameAttribute, }), WithDerefAliases(testCtx, NeverDerefAliases), ) }, updateWith: func(orig *AuthMethod) *AuthMethod { am := AllocAuthMethod() am.PublicId = orig.PublicId am.Urls = []string{"ldaps://ldap3", "ldaps://ldap4"} am.UserDn = "user-dn" am.UserAttr = "user-attr" am.UserFilter = "user-filter" am.GroupDn = "group-dn" am.GroupAttr = "group-attr" am.GroupFilter = "group-filter" am.BindDn = "bind-dn" am.BindPassword = "bind-password" am.Certificates = []string{testCertEncoded} am.ClientCertificate = testCertEncoded am.ClientCertificateKey = derPrivKey am.AccountAttributeMaps = []string{"cn=fullName"} am.DereferenceAliases = string(DerefAlways) return &am }, fieldMasks: []string{ UrlsField, UserDnField, UserAttrField, UserFilterField, GroupDnField, GroupAttrField, GroupFilterField, BindDnField, BindPasswordField, CertificatesField, ClientCertificateField, ClientCertificateKeyField, AccountAttributeMapsField, DerefAliasesField, }, version: 1, want: func(orig, updateWith *AuthMethod) *AuthMethod { am := orig.clone() am.Urls = updateWith.Urls am.UserDn = updateWith.UserDn am.UserAttr = updateWith.UserAttr am.UserFilter = updateWith.UserFilter am.GroupDn = updateWith.GroupDn am.GroupAttr = updateWith.GroupAttr am.GroupFilter = updateWith.GroupFilter am.BindDn = updateWith.BindDn am.BindPassword = updateWith.BindPassword am.BindPasswordHmac = updateWith.BindPasswordHmac am.ClientCertificateKey = updateWith.ClientCertificateKey am.ClientCertificate = updateWith.ClientCertificate am.ClientCertificateKeyHmac = updateWith.ClientCertificateKeyHmac am.AccountAttributeMaps = updateWith.AccountAttributeMaps am.DereferenceAliases = updateWith.DereferenceAliases return am }, }, { name: "remove-value-objects", ctx: testCtx, repo: testRepo, setup: func() *AuthMethod { return TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1", "ldap://ldap2"}, WithUserDn(testCtx, "orig-user-dn"), WithUserAttr(testCtx, "orig-user-attr"), WithUserFilter(testCtx, "orig-user-filter"), WithGroupDn(testCtx, "orig-group-dn"), WithGroupAttr(testCtx, "orig-group-attr"), WithGroupFilter(testCtx, "orig-group-filter"), WithBindCredential(testCtx, "orig-bind-dn", "orig-bind-password"), WithCertificates(testCtx, testCert), WithClientCertificate(testCtx, derPrivKey, testCert), // not a client cert but good enough for this test. WithAccountAttributeMap(testCtx, map[string]AccountToAttribute{ "mail": ToEmailAttribute, "cn": ToFullNameAttribute, }), WithDerefAliases(testCtx, DerefAlways), ) }, updateWith: func(orig *AuthMethod) *AuthMethod { am := AllocAuthMethod() am.PublicId = orig.PublicId return &am }, fieldMasks: []string{ UserDnField, UserAttrField, UserFilterField, GroupDnField, GroupAttrField, GroupFilterField, BindDnField, BindPasswordField, CertificatesField, ClientCertificateField, ClientCertificateKeyField, AccountAttributeMapsField, DerefAliasesField, }, version: 1, want: func(orig, updateWith *AuthMethod) *AuthMethod { am := orig.clone() am.Certificates = updateWith.Certificates am.UserDn = updateWith.UserDn am.UserAttr = updateWith.UserAttr am.UserFilter = updateWith.UserFilter am.GroupDn = updateWith.GroupDn am.GroupAttr = updateWith.GroupAttr am.GroupFilter = updateWith.GroupFilter am.BindDn = updateWith.BindDn am.BindPassword = updateWith.BindPassword am.BindPasswordHmac = updateWith.BindPasswordHmac am.ClientCertificateKey = updateWith.ClientCertificateKey am.ClientCertificate = updateWith.ClientCertificate am.ClientCertificateKeyHmac = updateWith.ClientCertificateKeyHmac am.AccountAttributeMaps = updateWith.AccountAttributeMaps am.DereferenceAliases = updateWith.DereferenceAliases return am }, }, { name: "update-just-binddn", ctx: testCtx, repo: testRepo, setup: func() *AuthMethod { return TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1", "ldap://ldap2"}, WithBindCredential(testCtx, "orig-bind-dn", "orig-bind-password"), ) }, updateWith: func(orig *AuthMethod) *AuthMethod { am := AllocAuthMethod() am.PublicId = orig.PublicId am.BindDn = "bind-dn" return &am }, fieldMasks: []string{ BindDnField, }, version: 1, want: func(orig, updateWith *AuthMethod) *AuthMethod { am := orig.clone() am.BindDn = updateWith.BindDn return am }, }, { name: "update-just-bind-password", ctx: testCtx, repo: testRepo, setup: func() *AuthMethod { return TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1", "ldap://ldap2"}, WithBindCredential(testCtx, "orig-bind-dn", "orig-bind-password"), ) }, updateWith: func(orig *AuthMethod) *AuthMethod { am := AllocAuthMethod() am.PublicId = orig.PublicId am.BindPassword = "bind-password" return &am }, fieldMasks: []string{ BindPasswordField, }, version: 1, want: func(orig, updateWith *AuthMethod) *AuthMethod { am := orig.clone() am.BindPassword = updateWith.BindPassword return am }, }, { name: "missing-auth-method", ctx: testCtx, repo: testRepo, setup: func() *AuthMethod { return nil }, updateWith: func(orig *AuthMethod) *AuthMethod { return orig }, wantErrMatch: errors.T(errors.InvalidParameter), wantErrContains: "missing auth method", }, { name: "missing-auth-method-store", ctx: testCtx, repo: testRepo, setup: func() *AuthMethod { return &AuthMethod{} }, updateWith: func(orig *AuthMethod) *AuthMethod { return orig }, wantErrMatch: errors.T(errors.InvalidParameter), wantErrContains: "missing auth method store", }, { name: "missing-public-id", ctx: testCtx, repo: testRepo, setup: func() *AuthMethod { am := AllocAuthMethod() return &am }, updateWith: func(orig *AuthMethod) *AuthMethod { return orig }, wantErrMatch: errors.T(errors.InvalidParameter), wantErrContains: "missing public id", }, { name: "invalid-field-mask", ctx: testCtx, repo: testRepo, fieldMasks: []string{"CreateTime"}, setup: func() *AuthMethod { am := AllocAuthMethod() am.PublicId = "test-id" return &am }, updateWith: func(orig *AuthMethod) *AuthMethod { return orig }, wantErrMatch: errors.T(errors.InvalidParameter), wantErrContains: "invalid field mask: \"CreateTime\"", }, { name: "no-field-mask", ctx: testCtx, repo: testRepo, setup: func() *AuthMethod { am := AllocAuthMethod() am.PublicId = "test-id" return &am }, updateWith: func(orig *AuthMethod) *AuthMethod { return orig }, wantErrMatch: errors.T(errors.EmptyFieldMask), wantErrContains: "empty field mask", }, { name: "missing-urls", ctx: testCtx, repo: testRepo, fieldMasks: []string{"Urls"}, setup: func() *AuthMethod { am := AllocAuthMethod() am.PublicId = "test-id" return &am }, updateWith: func(orig *AuthMethod) *AuthMethod { return orig }, wantErrMatch: errors.T(errors.InvalidParameter), wantErrContains: "missing urls (you cannot delete all of them; there must be at least one)", }, { name: "lookup-err", ctx: testCtx, repo: func() *Repository { conn, mock := db.TestSetupWithMock(t) mock.ExpectQuery(`SELECT`).WillReturnError(fmt.Errorf("lookup-err")) mockRw := db.New(conn) testRepo, err := NewRepository(testCtx, mockRw, mockRw, testKms) require.NoError(t, err) return testRepo }(), fieldMasks: []string{"UserDn"}, setup: func() *AuthMethod { am := AllocAuthMethod() am.PublicId = "test-id" return &am }, updateWith: func(orig *AuthMethod) *AuthMethod { return orig }, wantErrMatch: errors.T(errors.Unknown), wantErrContains: "lookup-err", }, { name: "not-found", ctx: testCtx, repo: testRepo, fieldMasks: []string{"UserDn"}, setup: func() *AuthMethod { am := AllocAuthMethod() am.PublicId = "test-id" return &am }, updateWith: func(orig *AuthMethod) *AuthMethod { return orig }, wantErrMatch: errors.T(errors.RecordNotFound), wantErrContains: "auth method \"test-id\": search issue", }, { name: "version-mismatch", ctx: testCtx, repo: testRepo, fieldMasks: []string{"UserDn"}, setup: func() *AuthMethod { am := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}) am.Version += 1 return am }, updateWith: func(orig *AuthMethod) *AuthMethod { return orig }, wantErrMatch: errors.T(errors.Integrity), wantErrContains: "update version 0 doesn't match db version 1", }, { name: "getWrapper-err", ctx: testCtx, repo: func() *Repository { testKms := &kms.MockGetWrapperer{ GetErr: fmt.Errorf("getWrapper-err"), } testRepo, err := NewRepository(testCtx, testRw, testRw, testKms) require.NoError(t, err) return testRepo }(), version: 1, fieldMasks: []string{"UserDn"}, setup: func() *AuthMethod { return TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}) }, updateWith: func(orig *AuthMethod) *AuthMethod { return orig }, wantErrMatch: errors.T(errors.Unknown), wantErrContains: "getWrapper-err", }, { name: "urls-conversion-err", ctx: testCtx, repo: testRepo, version: 1, fieldMasks: []string{"Urls"}, setup: func() *AuthMethod { am := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}) am.Urls = []string{"https://not-valid.com"} return am }, updateWith: func(orig *AuthMethod) *AuthMethod { return orig }, wantErrMatch: errors.T(errors.Unknown), wantErrContains: "valueObjectChanges: ldap.NewUrl: scheme \"https\" is not ldap or ldaps", }, { name: "certs-conversion-err", ctx: testCtx, repo: testRepo, version: 1, fieldMasks: []string{"Certificates"}, setup: func() *AuthMethod { am := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}) am.Certificates = []string{TestInvalidPem} return am }, updateWith: func(orig *AuthMethod) *AuthMethod { return orig }, wantErrMatch: errors.T(errors.Unknown), wantErrContains: "valueObjectChanges: ldap.NewCertificate: failed to parse certificate", }, { name: "account-maps-conversion-err", ctx: testCtx, repo: testRepo, version: 1, fieldMasks: []string{AccountAttributeMapsField}, setup: func() *AuthMethod { am := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}) am.AccountAttributeMaps = []string{"invalid-map"} return am }, updateWith: func(orig *AuthMethod) *AuthMethod { return orig }, wantErrMatch: errors.T(errors.Unknown), wantErrContains: "ldap.ParseAccountAttributeMaps: error parsing attribute", }, { name: "use-token-groups-update", ctx: testCtx, repo: testRepo, version: 1, fieldMasks: []string{"UseTokenGroups"}, setup: func() *AuthMethod { am := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}) return am }, updateWith: func(orig *AuthMethod) *AuthMethod { am := orig.clone() am.UseTokenGroups = true return am }, want: func(orig, updateWith *AuthMethod) *AuthMethod { am := orig.clone() am.UseTokenGroups = true return am }, }, { name: "use-token-groups-update-false", ctx: testCtx, repo: testRepo, version: 1, fieldMasks: []string{"UseTokenGroups"}, setup: func() *AuthMethod { am := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}, WithUseTokenGroups(testCtx)) return am }, updateWith: func(orig *AuthMethod) *AuthMethod { am := orig.clone() am.UseTokenGroups = false return am }, want: func(orig, updateWith *AuthMethod) *AuthMethod { am := orig.clone() am.UseTokenGroups = false return am }, }, { name: "start-tls-false", ctx: testCtx, repo: testRepo, version: 1, fieldMasks: []string{"StartTls"}, setup: func() *AuthMethod { am := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}, WithStartTLS(testCtx)) return am }, updateWith: func(orig *AuthMethod) *AuthMethod { am := orig.clone() am.StartTls = false return am }, want: func(orig, updateWith *AuthMethod) *AuthMethod { am := orig.clone() am.StartTls = false return am }, }, { name: "user-dn-update", ctx: testCtx, repo: testRepo, version: 1, fieldMasks: []string{"UserDn"}, setup: func() *AuthMethod { am := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}, WithUserDn(testCtx, "orig-user-dn")) return am }, updateWith: func(orig *AuthMethod) *AuthMethod { am := orig.clone() am.UserDn = "updated-user-dn" return am }, want: func(orig, updateWith *AuthMethod) *AuthMethod { am := orig.clone() am.UserDn = "updated-user-dn" return am }, }, { name: "user-attr-update", ctx: testCtx, repo: testRepo, version: 1, fieldMasks: []string{"UserAttr"}, setup: func() *AuthMethod { am := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}, WithUserDn(testCtx, "orig-user-dn"), WithUserAttr(testCtx, "orig-user-attr")) return am }, updateWith: func(orig *AuthMethod) *AuthMethod { am := orig.clone() am.UserAttr = "updated-user-attr" return am }, want: func(orig, updateWith *AuthMethod) *AuthMethod { am := orig.clone() am.UserAttr = "updated-user-attr" am.UserDn = "orig-user-dn" return am }, }, { name: "user-filter-update", ctx: testCtx, repo: testRepo, version: 1, fieldMasks: []string{"UserFilter"}, setup: func() *AuthMethod { am := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}, WithUserFilter(testCtx, "orig-user-filter")) return am }, updateWith: func(orig *AuthMethod) *AuthMethod { am := orig.clone() am.UserFilter = "updated-user-filter" return am }, want: func(orig, updateWith *AuthMethod) *AuthMethod { am := orig.clone() am.UserFilter = "updated-user-filter" return am }, }, { name: "enable-groups-err", ctx: testCtx, repo: testRepo, version: 1, fieldMasks: []string{"EnableGroups"}, setup: func() *AuthMethod { am := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}) return am }, updateWith: func(orig *AuthMethod) *AuthMethod { am := orig.clone() am.EnableGroups = true return am }, wantErrMatch: errors.T(errors.Integrity), wantErrContains: "must have a configured group_dn when enable_groups = true and use_token_groups = false", }, { name: "group-dn-update", ctx: testCtx, repo: testRepo, version: 1, fieldMasks: []string{"GroupDn"}, setup: func() *AuthMethod { am := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}, WithUserDn(testCtx, "orig-group-dn")) return am }, updateWith: func(orig *AuthMethod) *AuthMethod { am := orig.clone() am.GroupDn = "updated-group-dn" return am }, want: func(orig, updateWith *AuthMethod) *AuthMethod { am := orig.clone() am.GroupDn = "updated-group-dn" return am }, }, { name: "group-attr-update", ctx: testCtx, repo: testRepo, version: 1, fieldMasks: []string{"GroupAttr"}, setup: func() *AuthMethod { am := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}, WithGroupDn(testCtx, "orig-group-dn"), WithGroupAttr(testCtx, "orig-group-attr")) return am }, updateWith: func(orig *AuthMethod) *AuthMethod { am := orig.clone() am.GroupAttr = "updated-group-attr" return am }, want: func(orig, updateWith *AuthMethod) *AuthMethod { am := orig.clone() am.GroupAttr = "updated-group-attr" return am }, }, { name: "group-filter-update", ctx: testCtx, repo: testRepo, version: 1, fieldMasks: []string{"GroupAttr"}, setup: func() *AuthMethod { am := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}, WithGroupDn(testCtx, "orig-group-dn"), WithGroupFilter(testCtx, "orig-group-filter")) return am }, updateWith: func(orig *AuthMethod) *AuthMethod { am := orig.clone() am.GroupAttr = "updated-group-filter" return am }, want: func(orig, updateWith *AuthMethod) *AuthMethod { am := orig.clone() am.GroupAttr = "updated-group-filter" return am }, }, { name: "user-entry-search-conversion-no-update", ctx: testCtx, repo: testRepo, version: 1, fieldMasks: []string{"UserDn"}, setup: func() *AuthMethod { am := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}) return am }, updateWith: func(orig *AuthMethod) *AuthMethod { return orig }, want: func(orig, updateWith *AuthMethod) *AuthMethod { return orig.clone() }, wantNoRowsUpdated: true, }, { name: "group-entry-search-conversion-no-update", ctx: testCtx, repo: testRepo, version: 1, fieldMasks: []string{"GroupDn"}, setup: func() *AuthMethod { am := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}) return am }, updateWith: func(orig *AuthMethod) *AuthMethod { return orig }, want: func(orig, updateWith *AuthMethod) *AuthMethod { return orig.clone() }, wantNoRowsUpdated: true, }, { name: "client-search-conversion-no-update", ctx: testCtx, repo: testRepo, version: 1, fieldMasks: []string{"ClientCertificate"}, setup: func() *AuthMethod { am := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}) return am }, updateWith: func(orig *AuthMethod) *AuthMethod { return orig }, want: func(orig, updateWith *AuthMethod) *AuthMethod { return orig.clone() }, wantNoRowsUpdated: true, }, { name: "bind-credential-conversion-no-update", ctx: testCtx, repo: testRepo, version: 1, fieldMasks: []string{"BindDn"}, setup: func() *AuthMethod { am := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}) return am }, updateWith: func(orig *AuthMethod) *AuthMethod { return orig }, want: func(orig, updateWith *AuthMethod) *AuthMethod { return orig.clone() }, wantNoRowsUpdated: true, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) orig := tc.setup() updateWith := tc.updateWith(orig) updated, rowsUpdated, err := tc.repo.UpdateAuthMethod(tc.ctx, updateWith, tc.version, tc.fieldMasks, tc.opt...) if tc.wantErrMatch != nil { require.Error(err) assert.Empty(updated) assert.Zero(rowsUpdated) assert.Truef(errors.Match(tc.wantErrMatch, err), "want err code: %q got: %q", tc.wantErrMatch.Code, err) if tc.wantErrContains != "" { assert.Contains(err.Error(), tc.wantErrContains) } return } require.NoError(err) require.NotNil(updated) require.NotNil(tc.want) want := tc.want(orig, updateWith) want.CreateTime = updated.CreateTime want.UpdateTime = updated.UpdateTime want.Version = updated.Version want.BindPasswordHmac = updated.BindPasswordHmac want.ClientCertificateKeyHmac = updated.ClientCertificateKeyHmac TestSortAuthMethods(t, []*AuthMethod{want, updated}) assert.Empty(cmp.Diff(updated.AuthMethod, want.AuthMethod, protocmp.Transform())) if !tc.wantNoRowsUpdated { assert.Equal(1, rowsUpdated) err = db.TestVerifyOplog(t, testRw, updateWith.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) require.NoErrorf(err, "unexpected error verifying oplog entry: %s", err) } found, err := tc.repo.LookupAuthMethod(tc.ctx, want.PublicId) require.NoError(err) TestSortAuthMethods(t, []*AuthMethod{found}) assert.Empty(cmp.Diff(found.AuthMethod, want.AuthMethod, protocmp.Transform())) }) } } func Test_validateFieldMask(t *testing.T) { t.Parallel() tests := []struct { name string fieldMask []string wantErr bool }{ { name: "all-valid-fields", fieldMask: []string{ NameField, DescriptionField, StartTlsField, InsecureTlsField, DiscoverDnField, AnonGroupSearchField, UpnDomainField, UrlsField, UserDnField, UserAttrField, UserFilterField, GroupDnField, GroupAttrField, GroupFilterField, CertificatesField, ClientCertificateField, ClientCertificateKeyField, BindDnField, BindPasswordField, AccountAttributeMapsField, }, }, { name: "invalid", fieldMask: []string{"Invalid", NameField}, wantErr: true, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { require := require.New(t) err := validateFieldMask(context.TODO(), tc.fieldMask) if tc.wantErr { require.Error(err) return } require.NoError(err) }) } } // Test_valueObjectChanges is just being used to test failure conditions primarily func Test_valueObjectChanges(t *testing.T) { t.Parallel() testCtx := context.Background() _, pem1 := TestGenerateCA(t, "localhost") _, pem2 := TestGenerateCA(t, "127.0.0.1") _, pem3 := TestGenerateCA(t, "www.example.com") tests := []struct { name string ctx context.Context id string voName voName new []string old []string dbMask []string nullFields []string wantAdd []any wantDel []any wantErrMatch *errors.Template wantErrContains string }{ { name: "missing-public-id", ctx: testCtx, voName: CertificateVO, new: nil, old: []string{pem1, pem2, pem3}, nullFields: []string{string(CertificateVO)}, wantErrMatch: errors.T(errors.InvalidParameter), wantErrContains: "missing public id", }, { name: "invalid-vo-name", ctx: testCtx, voName: voName("invalid-name"), id: "am-public-id", new: nil, old: []string{pem1, pem2}, nullFields: []string{string(CertificateVO)}, wantErrMatch: errors.T(errors.InvalidParameter), wantErrContains: "invalid value object name", }, { name: "dup-new", ctx: testCtx, id: "am-public-id", voName: CertificateVO, new: []string{pem1, pem1}, old: []string{pem1}, dbMask: []string{string(CertificateVO)}, wantErrMatch: errors.T(errors.InvalidParameter), wantErrContains: "duplicate new Certificates", }, { name: "dup-old", ctx: testCtx, id: "am-public-id", voName: CertificateVO, new: []string{pem1}, old: []string{pem2, pem2}, dbMask: []string{string(CertificateVO)}, wantErrMatch: errors.T(errors.InvalidParameter), wantErrContains: "duplicate old Certificates", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) gotAdd, gotDel, err := valueObjectChanges(tc.ctx, tc.id, tc.voName, tc.new, tc.old, tc.dbMask, tc.nullFields) if tc.wantErrMatch != nil { require.Error(err) assert.Truef(errors.Match(tc.wantErrMatch, err), "want err code: %q got: %q", tc.wantErrMatch.Code, err) if tc.wantErrContains != "" { assert.Contains(err.Error(), tc.wantErrContains) } return } require.NoError(err) assert.Equal(tc.wantAdd, gotAdd) switch tc.voName { case CertificateVO: sort.Slice(gotDel, func(a, b int) bool { aa := gotDel[a] bb := gotDel[b] return aa.(*Certificate).Cert < bb.(*Certificate).Cert }) case UrlVO: sort.Slice(gotDel, func(a, b int) bool { aa := gotDel[a] bb := gotDel[b] return aa.(*Url).ServerUrl < bb.(*Url).ServerUrl }) case AccountAttributeMapsVO: sort.Slice(gotDel, func(a, b int) bool { aa := gotDel[a] bb := gotDel[b] return aa.(*AccountAttributeMap).ToAttribute < bb.(*AccountAttributeMap).ToAttribute }) } assert.Equalf(tc.wantDel, gotDel, "wantDel: %s\ngotDel: %s\n", tc.wantDel, gotDel) }) } }