// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package ldap import ( "context" "fmt" "net/url" "reflect" "strings" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/oplog" "github.com/hashicorp/go-dbw" "github.com/hashicorp/go-secure-stdlib/strutil" ) const ( OperationalStateField = "OperationalState" VersionField = "Version" IsPrimaryAuthMethodField = "IsPrimaryAuthMethod" NameField = "Name" DescriptionField = "Description" StartTlsField = "StartTls" InsecureTlsField = "InsecureTls" DiscoverDnField = "DiscoverDn" AnonGroupSearchField = "AnonGroupSearch" UpnDomainField = "UpnDomain" UrlsField = "Urls" UserDnField = "UserDn" UserAttrField = "UserAttr" UserFilterField = "UserFilter" EnableGroupsField = "EnableGroups" UseTokenGroupsField = "UseTokenGroups" GroupDnField = "GroupDn" GroupAttrField = "GroupAttr" GroupFilterField = "GroupFilter" CertificatesField = "Certificates" ClientCertificateField = "ClientCertificate" ClientCertificateKeyField = "ClientCertificateKey" BindDnField = "BindDn" BindPasswordField = "BindPassword" AccountAttributeMapsField = "AccountAttributeMaps" GroupNamesField = "GroupNames" DerefAliasesField = "DereferenceAliases" MaximumPageSizeField = "MaximumPageSize" ) // isEmpty returns true if all the args are empty. Only supports checking // strings and pointers, all other types are assumed to be empty. func isEmpty(args ...any) bool { for _, i := range args { switch v := reflect.ValueOf(i); v.Kind() { case reflect.Pointer: if !v.IsNil() { return false } case reflect.String: if v.String() != "" { return false } } } return true } // UpdateAuthMethod will retrieve the auth method from the repository, // and update it based on the field masks provided. // // fieldMaskPaths provides field_mask.proto paths for fields that should // be updated. Fields will be set to NULL if the field is a // zero value and included in fieldMask. Name, Description, StartTLs, // DiscoverDn, AnonGroupSearch, UpnDomain, UserDn, UserAttr, UserFilter, // GroupDn, GroupAttr, GroupFilter, ClientCertificateKey, ClientCertificate, // BindDn and BindPassword are all updatable fields. The AuthMethod's Value // Objects of Urls and Certificates are also updatable. If no updatable fields // are included in the fieldMaskPaths, then an error is returned. // // No Options are currently supported. func (r *Repository) UpdateAuthMethod(ctx context.Context, am *AuthMethod, version uint32, fieldMaskPaths []string, _ ...Option) (*AuthMethod, int, error) { const op = "ldap.(AuthMethod).Update" switch { case am == nil: return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing auth method") case am.AuthMethod == nil: return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing auth method store") case am.PublicId == "": return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing public id") } if err := validateFieldMask(ctx, fieldMaskPaths); err != nil { return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op) } dbMask, nullFields := dbw.BuildUpdatePaths( map[string]any{ OperationalStateField: am.OperationalState, NameField: am.Name, DescriptionField: am.Description, StartTlsField: am.StartTls, InsecureTlsField: am.InsecureTls, DiscoverDnField: am.DiscoverDn, AnonGroupSearchField: am.AnonGroupSearch, UpnDomainField: am.UpnDomain, UserDnField: am.UserDn, UserAttrField: am.UserAttr, UserFilterField: am.UserFilter, EnableGroupsField: am.EnableGroups, UseTokenGroupsField: am.UseTokenGroups, GroupDnField: am.GroupDn, GroupAttrField: am.GroupAttr, GroupFilterField: am.GroupFilter, CertificatesField: am.Certificates, ClientCertificateField: am.ClientCertificate, ClientCertificateKeyField: am.ClientCertificateKey, BindDnField: am.BindDn, BindPasswordField: am.BindPassword, UrlsField: am.Urls, AccountAttributeMapsField: am.AccountAttributeMaps, DerefAliasesField: am.DereferenceAliases, MaximumPageSizeField: am.MaximumPageSize, }, fieldMaskPaths, []string{ StartTlsField, InsecureTlsField, DiscoverDnField, AnonGroupSearchField, EnableGroupsField, UseTokenGroupsField, MaximumPageSizeField, }, ) if len(dbMask) == 0 && len(nullFields) == 0 { return nil, db.NoRowsAffected, errors.New(ctx, errors.EmptyFieldMask, op, "empty field mask") } if strutil.StrListContains(nullFields, UrlsField) { return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing urls (you cannot delete all of them; there must be at least one)") } origAm, err := r.LookupAuthMethod(ctx, am.PublicId) if err != nil { return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("%q auth method not found", am.PublicId)) } if origAm == nil { return nil, db.NoRowsAffected, errors.New(ctx, errors.RecordNotFound, op, fmt.Sprintf("auth method %q", am.PublicId)) } // there's no reason to continue if another controller has already updated this auth method. if origAm.Version != version { return nil, db.NoRowsAffected, errors.New(ctx, errors.VersionMismatch, op, fmt.Sprintf("update version %d doesn't match db version %d", version, origAm.Version)) } dbWrapper, err := r.kms.GetWrapper(ctx, origAm.ScopeId, kms.KeyPurposeDatabase) if err != nil { return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper")) } au, du, err := valueObjectChanges(ctx, origAm.PublicId, UrlVO, am.Urls, origAm.Urls, dbMask, nullFields) if err != nil { return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update urls")) } addUrls := []*Url{} for _, u := range au { addUrls = append(addUrls, u.(*Url)) } deleteUrls := []*Url{} for _, u := range du { deleteUrls = append(deleteUrls, u.(*Url)) } ac, dc, err := valueObjectChanges(ctx, origAm.PublicId, CertificateVO, am.Certificates, origAm.Certificates, dbMask, nullFields) if err != nil { return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update certificates")) } deleteCerts := []*Certificate{} for _, c := range dc { deleteCerts = append(deleteCerts, c.(*Certificate)) } addCerts := []*Certificate{} for _, c := range ac { addCerts = append(addCerts, c.(*Certificate)) } addM, deleteM, err := valueObjectChanges(ctx, origAm.PublicId, AccountAttributeMapsVO, am.AccountAttributeMaps, origAm.AccountAttributeMaps, dbMask, nullFields) if err != nil { return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update account attribute maps")) } addMaps := []*AccountAttributeMap{} for _, m := range addM { addMaps = append(addMaps, m.(*AccountAttributeMap)) } deleteMaps := []*AccountAttributeMap{} for _, m := range deleteM { deleteMaps = append(deleteMaps, m.(*AccountAttributeMap)) } combinedMasks := append(dbMask, nullFields...) var addUserSearchConf, deleteUserSearchConf any if strListContainsOneOf(combinedMasks, UserDnField, UserAttrField, UserFilterField) { if !isEmpty(origAm.UserDn, origAm.UserAttr, origAm.UserFilter) { usc := allocUserEntrySearchConf() usc.LdapMethodId = am.PublicId deleteUserSearchConf = usc } userDn := origAm.UserDn switch { case strutil.StrListContains(dbMask, UserDnField): userDn = am.UserDn case strutil.StrListContains(nullFields, UserDnField): userDn = "" } userAttr := origAm.UserAttr switch { case strutil.StrListContains(dbMask, UserAttrField): userAttr = am.UserAttr case strutil.StrListContains(nullFields, UserAttrField): userAttr = "" } userFilter := origAm.UserFilter switch { case strutil.StrListContains(dbMask, UserFilterField): userFilter = am.UserFilter case strutil.StrListContains(nullFields, UserFilterField): userFilter = "" } if !isEmpty(userDn, userAttr, userFilter) { addUserSearchConf, err = NewUserEntrySearchConf(ctx, am.PublicId, WithUserDn(ctx, userDn), WithUserAttr(ctx, userAttr), WithUserFilter(ctx, userFilter)) if err != nil { return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update user search configuration")) } } } var addGroupSearchConf, deleteGroupSearchConf any if strListContainsOneOf(combinedMasks, GroupDnField, GroupAttrField, GroupFilterField) { if !isEmpty(origAm.GroupDn, origAm.GroupAttr, origAm.GroupFilter) { gsc := allocGroupEntrySearchConf() gsc.LdapMethodId = am.PublicId deleteGroupSearchConf = gsc } groupDn := origAm.GroupDn switch { case strutil.StrListContains(dbMask, GroupDnField): groupDn = am.GroupDn case strutil.StrListContains(nullFields, GroupDnField): groupDn = "" } groupAttr := origAm.GroupAttr switch { case strutil.StrListContains(dbMask, GroupAttrField): groupAttr = am.GroupAttr case strutil.StrListContains(nullFields, GroupAttrField): groupAttr = "" } groupFilter := origAm.GroupFilter switch { case strutil.StrListContains(dbMask, GroupFilterField): groupFilter = am.GroupFilter case strutil.StrListContains(nullFields, GroupFilterField): groupFilter = "" } if !isEmpty(groupDn, groupAttr, groupFilter) { addGroupSearchConf, err = NewGroupEntrySearchConf(ctx, am.PublicId, WithGroupDn(ctx, groupDn), WithGroupAttr(ctx, groupAttr), WithGroupFilter(ctx, groupFilter)) if err != nil { return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update group search configuration")) } } } var addClientCert, deleteClientCert any if strListContainsOneOf(combinedMasks, ClientCertificateField, ClientCertificateKeyField) { if !isEmpty(origAm.ClientCertificate, origAm.ClientCertificateKey) { cc := allocClientCertificate() cc.LdapMethodId = am.PublicId deleteClientCert = cc } clientCertificate := origAm.ClientCertificate switch { case strutil.StrListContains(dbMask, ClientCertificateField): clientCertificate = am.ClientCertificate case strutil.StrListContains(nullFields, ClientCertificateField): clientCertificate = "" } clientCertificateKey := origAm.ClientCertificateKey switch { case strutil.StrListContains(dbMask, ClientCertificateKeyField): clientCertificateKey = am.ClientCertificateKey case strutil.StrListContains(nullFields, ClientCertificateKeyField): clientCertificateKey = nil } if !isEmpty(clientCertificate, clientCertificateKey) { cc, err := NewClientCertificate(ctx, am.PublicId, clientCertificateKey, clientCertificate) if err != nil { return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op) } if err := cc.encrypt(ctx, dbWrapper); err != nil { return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op) } addClientCert = cc } } var addBindCred, deleteBindCred any if strListContainsOneOf(combinedMasks, BindDnField, BindPasswordField) { if !isEmpty(origAm.BindDn, origAm.BindPassword) { bc := allocBindCredential() bc.LdapMethodId = am.PublicId deleteBindCred = bc } bindDn := origAm.BindDn switch { case strutil.StrListContains(dbMask, BindDnField): bindDn = am.BindDn case strutil.StrListContains(nullFields, BindDnField): bindDn = "" } bindPassword := origAm.BindPassword switch { case strutil.StrListContains(dbMask, BindPasswordField): bindPassword = am.BindPassword case strutil.StrListContains(nullFields, BindPasswordField): bindPassword = "" } if !isEmpty(bindDn, bindPassword) { bc, err := NewBindCredential(ctx, am.PublicId, bindDn, []byte(bindPassword)) if err != nil { return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op) } if err := bc.encrypt(ctx, dbWrapper); err != nil { return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("error wrapping updated bind credential")) } addBindCred = bc } } var addDerefAliases, deleteDerefAliases any if strListContainsOneOf(combinedMasks, DerefAliasesField) { if !isEmpty(origAm.DereferenceAliases) { d := allocDerefAliases() d.LdapMethodId = am.PublicId deleteDerefAliases = d } derefAliases := origAm.DereferenceAliases switch { case strutil.StrListContains(dbMask, DerefAliasesField): derefAliases = am.DereferenceAliases case strutil.StrListContains(nullFields, DerefAliasesField): derefAliases = "" } if !isEmpty(derefAliases) { d, err := NewDerefAliases(ctx, am.PublicId, DerefAliasType(derefAliases)) if err != nil { return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op) } addDerefAliases = d } } var filteredDbMask, filteredNullFields []string for _, f := range dbMask { switch f { case UrlsField, CertificatesField, AccountAttributeMapsField, UserDnField, UserAttrField, UserFilterField, GroupDnField, GroupAttrField, GroupFilterField, ClientCertificateField, ClientCertificateKeyField, BindDnField, BindPasswordField, DerefAliasesField: continue default: filteredDbMask = append(filteredDbMask, f) } } for _, f := range nullFields { switch f { case StartTlsField, InsecureTlsField, DiscoverDnField, AnonGroupSearchField, EnableGroupsField, UseTokenGroupsField, UrlsField, CertificatesField, AccountAttributeMapsField, UserDnField, UserAttrField, UserFilterField, GroupDnField, GroupAttrField, GroupFilterField, ClientCertificateField, ClientCertificateKeyField, BindDnField, BindPasswordField, DerefAliasesField: continue default: filteredNullFields = append(filteredNullFields, f) } } // handle no changes... if len(filteredDbMask) == 0 && len(filteredNullFields) == 0 && len(addUrls) == 0 && len(deleteUrls) == 0 && len(addCerts) == 0 && len(deleteCerts) == 0 && addUserSearchConf == nil && deleteUserSearchConf == nil && addGroupSearchConf == nil && deleteGroupSearchConf == nil && addClientCert == nil && deleteClientCert == nil && addBindCred == nil && deleteBindCred == nil && len(addMaps) == 0 && len(deleteMaps) == 0 && addDerefAliases == nil && deleteDerefAliases == nil { return origAm, db.NoRowsAffected, nil } oplogWrapper, err := r.kms.GetWrapper(ctx, origAm.ScopeId, kms.KeyPurposeOplog) if err != nil { return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) } var updatedAm *AuthMethod var rowsUpdated int _, err = r.writer.DoTx( ctx, db.StdRetryCnt, db.ExpBackoff{}, func(reader db.Reader, w db.Writer) error { msgs := make([]*oplog.Message, 0, 7) // AuthMethod, Algs*2, Certs*2, Audiences*2 ticket, err := w.GetTicket(ctx, am) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket")) } var authMethodOplogMsg oplog.Message switch { case len(filteredDbMask) == 0 && len(filteredNullFields) == 0: // the auth method's fields are not being updated, just it's value objects, so we need to just update the auth // method's version. updatedAm = am.clone() updatedAm.Version = uint32(version) + 1 rowsUpdated, err = w.Update(ctx, updatedAm, []string{VersionField}, nil, db.NewOplogMsg(&authMethodOplogMsg), db.WithVersion(&version)) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update auth method version")) } if rowsUpdated != 1 { return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated auth method version and %d rows updated", rowsUpdated)) } default: updatedAm = am.clone() rowsUpdated, err = w.Update(ctx, updatedAm, filteredDbMask, filteredNullFields, db.NewOplogMsg(&authMethodOplogMsg), db.WithVersion(&version)) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update auth method")) } if rowsUpdated != 1 { return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated auth method and %d rows updated", rowsUpdated)) } } msgs = append(msgs, &authMethodOplogMsg) if len(deleteCerts) > 0 { deleteCertOplogMsgs := make([]*oplog.Message, 0, len(deleteCerts)) rowsDeleted, err := w.DeleteItems(ctx, deleteCerts, db.NewOplogMsgs(&deleteCertOplogMsgs)) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete certificates")) } if rowsDeleted != len(deleteCerts) { return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("certificates deleted %d did not match request for %d", rowsDeleted, len(deleteCerts))) } msgs = append(msgs, deleteCertOplogMsgs...) } if len(addCerts) > 0 { addCertsOplogMsgs := make([]*oplog.Message, 0, len(addCerts)) if err := w.CreateItems(ctx, addCerts, db.NewOplogMsgs(&addCertsOplogMsgs)); err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add certificates")) } msgs = append(msgs, addCertsOplogMsgs...) } if len(deleteUrls) > 0 { deleteAudsOplogMsgs := make([]*oplog.Message, 0, len(deleteUrls)) rowsDeleted, err := w.DeleteItems(ctx, deleteUrls, db.NewOplogMsgs(&deleteAudsOplogMsgs)) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete URLs")) } if rowsDeleted != len(deleteUrls) { return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("urls deleted %d did not match request for %d", rowsDeleted, len(deleteUrls))) } msgs = append(msgs, deleteAudsOplogMsgs...) } if len(addUrls) > 0 { addUrlsOplogMsgs := make([]*oplog.Message, 0, len(addUrls)) if err := w.CreateItems(ctx, addUrls, db.NewOplogMsgs(&addUrlsOplogMsgs)); err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add urls")) } msgs = append(msgs, addUrlsOplogMsgs...) } if deleteUserSearchConf != nil { var deleteUserSearchConfMsg oplog.Message rowsDeleted, err := w.Delete(ctx, deleteUserSearchConf, db.NewOplogMsg(&deleteUserSearchConfMsg)) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete user search conf")) } if rowsDeleted != 1 { return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("user search conf deleted %d did not match request for 1", rowsDeleted)) } msgs = append(msgs, &deleteUserSearchConfMsg) } if addUserSearchConf != nil { var addUserSearchConfOplogMsg oplog.Message if err := w.Create(ctx, addUserSearchConf, db.NewOplogMsg(&addUserSearchConfOplogMsg)); err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add user search conf")) } msgs = append(msgs, &addUserSearchConfOplogMsg) } if deleteGroupSearchConf != nil { var deleteGroupSearchConfMsg oplog.Message rowsDeleted, err := w.Delete(ctx, deleteGroupSearchConf, db.NewOplogMsg(&deleteGroupSearchConfMsg)) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete group search conf")) } if rowsDeleted != 1 { return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("group search conf deleted %d did not match request for 1", rowsDeleted)) } msgs = append(msgs, &deleteGroupSearchConfMsg) } if addGroupSearchConf != nil { var addGroupSearchConfOplogMsg oplog.Message if err := w.Create(ctx, addGroupSearchConf, db.NewOplogMsg(&addGroupSearchConfOplogMsg)); err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add group search conf")) } msgs = append(msgs, &addGroupSearchConfOplogMsg) } if deleteClientCert != nil { var deleteClientCertMsg oplog.Message rowsDeleted, err := w.Delete(ctx, deleteClientCert, db.NewOplogMsg(&deleteClientCertMsg)) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete client cert")) } if rowsDeleted != 1 { return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("client cert deleted %d did not match request for 1", rowsDeleted)) } msgs = append(msgs, &deleteClientCertMsg) } if addClientCert != nil { var addClientCertOplogMsg oplog.Message if err := w.Create(ctx, addClientCert, db.NewOplogMsg(&addClientCertOplogMsg)); err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add client cert")) } msgs = append(msgs, &addClientCertOplogMsg) } if deleteBindCred != nil { var deleteBindCredMsg oplog.Message rowsDeleted, err := w.Delete(ctx, deleteBindCred, db.NewOplogMsg(&deleteBindCredMsg)) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete bind credential conf")) } if rowsDeleted != 1 { return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("bind credential deleted %d did not match request for 1", rowsDeleted)) } msgs = append(msgs, &deleteBindCredMsg) } if addBindCred != nil { var addBindCredOplogMsg oplog.Message if err := w.Create(ctx, addBindCred, db.NewOplogMsg(&addBindCredOplogMsg)); err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add bind credential")) } msgs = append(msgs, &addBindCredOplogMsg) } if len(deleteMaps) > 0 { deleteMapsOplogMsgs := make([]*oplog.Message, 0, len(deleteMaps)) rowsDeleted, err := w.DeleteItems(ctx, deleteMaps, db.NewOplogMsgs(&deleteMapsOplogMsgs)) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete account attribute maps")) } if rowsDeleted != len(deleteMaps) { return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("account attribute maps deleted %d did not match request for %d", rowsDeleted, len(deleteMaps))) } msgs = append(msgs, deleteMapsOplogMsgs...) } if len(addMaps) > 0 { addMapsOplogMsgs := make([]*oplog.Message, 0, len(addMaps)) if err := w.CreateItems(ctx, addMaps, db.NewOplogMsgs(&addMapsOplogMsgs)); err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add account attribute maps")) } msgs = append(msgs, addMapsOplogMsgs...) } if deleteDerefAliases != nil { var deleteDerefMsg oplog.Message rowsDeleted, err := w.Delete(ctx, deleteDerefAliases, db.NewOplogMsg(&deleteDerefMsg)) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete deref aliases")) } if rowsDeleted != 1 { return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("deref aliases deleted %d did not match request for 1", rowsDeleted)) } msgs = append(msgs, &deleteDerefMsg) } if addDerefAliases != nil { var addDerefOplogMsg oplog.Message if err := w.Create(ctx, addDerefAliases, db.NewOplogMsg(&addDerefOplogMsg)); err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add deref aliases")) } msgs = append(msgs, &addDerefOplogMsg) } metadata, err := updatedAm.oplog(ctx, oplog.OpType_OP_TYPE_UPDATE) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to generate oplog metadata")) } if err := w.WriteOplogEntryWith(ctx, oplogWrapper, ticket, metadata, msgs); err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog")) } // we need a new repo, that's using the same reader/writer as this TxHandler txRepo := &Repository{ reader: reader, writer: w, kms: r.kms, // intentionally not setting the defaultLimit, so we'll get all // the account ids without a limit } updatedAm, err = txRepo.lookupAuthMethod(ctx, updatedAm.PublicId) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to lookup auth method after update")) } if updatedAm == nil { return errors.New(ctx, errors.RecordNotFound, op, "unable to lookup auth method after update") } return nil }, ) if err != nil { return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op) } return updatedAm, rowsUpdated, nil } // validateFieldMasks ensures that all the fields in the mask are updatable func validateFieldMask(ctx context.Context, fieldMaskPaths []string) error { const op = "ldap.validateFieldMasks" for _, f := range fieldMaskPaths { switch { case strings.EqualFold(OperationalStateField, f): case strings.EqualFold(NameField, f): case strings.EqualFold(DescriptionField, f): case strings.EqualFold(StartTlsField, f): case strings.EqualFold(InsecureTlsField, f): case strings.EqualFold(DiscoverDnField, f): case strings.EqualFold(AnonGroupSearchField, f): case strings.EqualFold(UpnDomainField, f): case strings.EqualFold(UserDnField, f): case strings.EqualFold(UserAttrField, f): case strings.EqualFold(UserFilterField, f): case strings.EqualFold(EnableGroupsField, f): case strings.EqualFold(UseTokenGroupsField, f): case strings.EqualFold(GroupDnField, f): case strings.EqualFold(GroupAttrField, f): case strings.EqualFold(GroupFilterField, f): case strings.EqualFold(CertificatesField, f): case strings.EqualFold(ClientCertificateField, f): case strings.EqualFold(ClientCertificateKeyField, f): case strings.EqualFold(BindDnField, f): case strings.EqualFold(BindPasswordField, f): case strings.EqualFold(UrlsField, f): case strings.EqualFold(AccountAttributeMapsField, f): case strings.EqualFold(DerefAliasesField, f): case strings.EqualFold(MaximumPageSizeField, f): default: return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("invalid field mask: %q", f)) } } return nil } // voName represents the names of auth method value objects type voName string const ( CertificateVO voName = "Certificates" UrlVO voName = "Urls" AccountAttributeMapsVO voName = "AccountAttributeMaps" ) // validVoName decides if the name is valid func validVoName(name voName) bool { switch name { case CertificateVO, UrlVO, AccountAttributeMapsVO: return true default: return false } } // factoryFunc defines a func type for value object factories type factoryFunc func(ctx context.Context, publicId string, idx int, s string) (any, error) // supportedFactories are the currently supported factoryFunc for value objects var supportedFactories = map[voName]factoryFunc{ CertificateVO: func(ctx context.Context, publicId string, idx int, s string) (any, error) { return NewCertificate(ctx, publicId, s) }, UrlVO: func(ctx context.Context, publicId string, idx int, s string) (any, error) { u, err := url.Parse(s) if err != nil { return nil, errors.Wrap(ctx, err, "ldap.urlFactory") } return NewUrl(ctx, publicId, idx+1, u) }, AccountAttributeMapsVO: func(ctx context.Context, publicId string, idx int, s string) (any, error) { const op = "ldap.AccountAttributeMapsFactory" acm, err := ParseAccountAttributeMaps(ctx, s) if err != nil { return nil, err } if len(acm) > 1 { return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unable to parse account attribute map %s", s)) } var m AttributeMap for _, m = range acm { } to, err := ConvertToAccountToAttribute(ctx, m.To) if err != nil { return nil, errors.Wrap(ctx, err, op) } return NewAccountAttributeMap(ctx, publicId, m.From, to) }, } // valueObjectChanges takes the new and old list of VOs (value objects) and // using the dbMasks/nullFields it will return lists of VOs which need to be // added and deleted in order to reconcile auth method's value objects. func valueObjectChanges( ctx context.Context, publicId string, valueObjectName voName, newVOs, oldVOs, dbMask, nullFields []string, ) (add []any, del []any, e error) { const op = "ldap.valueObjectChanges" switch { case publicId == "": return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing public id") case !validVoName(valueObjectName): return nil, nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("invalid value object name: %s", valueObjectName)) case !strutil.StrListContains(dbMask, string(valueObjectName)) && !strutil.StrListContains(nullFields, string(valueObjectName)): return nil, nil, nil case len(strutil.RemoveDuplicates(newVOs, false)) != len(newVOs): return nil, nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("duplicate new %s", valueObjectName)) case len(strutil.RemoveDuplicates(oldVOs, false)) != len(oldVOs): return nil, nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("duplicate old %s", valueObjectName)) } factory, ok := supportedFactories[valueObjectName] if !ok { return nil, nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unsupported factory for value object: %s", valueObjectName)) } foundVOs := map[string]int{} for i, a := range oldVOs { foundVOs[a] = i } var adds []any var deletes []any if strutil.StrListContains(nullFields, string(valueObjectName)) { deletes = make([]any, 0, len(oldVOs)) for i, v := range oldVOs { deleteObj, err := factory(ctx, publicId, i, v) if err != nil { return nil, nil, errors.Wrap(ctx, err, op) } deletes = append(deletes, deleteObj) delete(foundVOs, v) } } if strutil.StrListContains(dbMask, string(valueObjectName)) { adds = make([]any, 0, len(newVOs)) for i, v := range newVOs { if _, ok := foundVOs[v]; ok { delete(foundVOs, v) continue } obj, err := factory(ctx, publicId, i, v) if err != nil { return nil, nil, errors.Wrap(ctx, err, op) } adds = append(adds, obj) delete(foundVOs, v) } } if len(foundVOs) > 0 { for v := range foundVOs { obj, err := factory(ctx, publicId, foundVOs[v], v) if err != nil { return nil, nil, errors.Wrap(ctx, err, op) } deletes = append(deletes, obj) delete(foundVOs, v) } } return adds, deletes, nil } func strListContainsOneOf(haystack []string, needles ...string) bool { for _, item := range haystack { for _, n := range needles { if item == n { return true } } } return false }