Files
threshold/internal/auth/ldap/repository_auth_method_update.go
T
Jim 86192f75eb feature (auth/ldap): add LDAP auth method along with associated accounts and managed groups (#2912)
* feature (auth): required schema changes for auth ldap method (#2669)

chore (auth/ldap): move schema changes to next avail migration number

* feature (auth/ldap): define AuthMethod and all its value objects (#2703)

* feature (auth/ldap): storage protos
* feature (auth/ldap): define AuthMethod and all its value objects

* feature (auth/ldap): add repo and reading an auth method (#2718)

* feature (auth/ldap): add Repository.CreateAuthMethod(...) and Repository.DeleteAuthMethod(...) (#2724)

* feature (auth/ldap): add Repository.UpdateAuthMethod(...) (#2739)

* feature (auth/ldap): add Account

* feature (auth/ldap): add AuthMethod.EnableGroups

* fix (auth/ldap): refactor AuthMethod.oplog to enforce proper constraints

* feature (auth/ldap): add Account repo functions

* refactor (auth/ldap): remove entry attributes from account

Realized the entry attributes could have absolutely anything in them
(including binary data) and since we absolutely don't have to have them
there's just no reason to take on the risk.

* feature (auth/ldap): add AuthMethod.UseTokenGroups

* feature (auth/ldap): add Authenticate(...)

* refactor (auth/ldap): ensure options take a context as the 1st parameter

* feature (auth/ldap): add Account attribute maps

* chore (auth/ldap): make fmt deltas

* feature (auth/ldap): add managed groups (#2760)

* tests (auth/ldap): add missing unit test to Repository.DeleteAccount(...)

Add bits to test the delete operation when you're not able to generate
oplog metadata

* feature (auth/ldap): add managed groups

fixup! feature (auth/ldap): add managed groups (#2760)

* feature (auth/ldap): service proto definition (#2761)

* feature (handlers/authmethods): add handlers for ldap auth method operations (#2794)

* feature (auth/ldap): add Account attribute maps

* chore (auth/ldap): update cap/ldap to latest version

* feature (auth/ldap): add ldap api generation definitions

* feature (authmethods): add ldap repo NewService(...)

* feature (authmethods/ldap): add proper mask_mapping to protobufs

* feature (authmethods): add support to get an ldap auth method

* refactor (auth/ldap): export TestGenerateCA(...)

* feature (authmethods): add support to create an ldap auth method

* feature (authmethods): add support to delete an ldap auth method

* feature (authmethods): add support to list ldap auth methods

* refactor (auth/ldap): make urls optional for NewAuthMethod(...)

* refactor (auth/ldap): export ldap.TestInvalidPem

* chore: make fmt changes

* fix (auth/ldap): properly handle group search config

Add constraints and tests to ensure when an ldap AuthMethod.EnableGroups
is true, and UseTokenGroups is false; that there's a GroupDn configured
for finding a user's associated groups

* feature (authmethods): add support to update an ldap auth methods

* chore (db/ldap): tmp mv migrations so there's no conflict with ongoing work

* feature (verifier): add ldap auth method to verifier bits

* fix (controller): prevent panic when controller stops when there's no listener

* feature (authmethods): add support to authenticate via ldap auth methods

* chore (migrations): fix whitespace in stmt

* chore: fmt fixup

* tests (auth/ldap): invalid err msg

* feature (cli/authmethods): add support for ldap auth-methods CRUD and authenticate (#2810)

* feature (authmethods): add CLI support for ldap auth methods CRUD

* tests (api/auth): ldap auth method classification tests

* feature (authmethods): add CLI support for ldap auth authenticate

* feature (auth/ldap): set request timeouts for ldap server connections

* feature (handlers/authmethods): handle u_anon listing properly.

* feature (account/handers): ldap account and managed group CRUDL APIs (#2852)

* feature (auth/ldap) add repository Listing of ManagedGroupMemberAccount

* feature (controller/handlers): add ldapRepoFn to accounts service

* feature (auth/ldap) register ldap managed group subtype

* feature (account/handlers): ldap account CRUDL APIs

* feature (controller/handlers): add ldapRepo to managed groups service

* feature (account/handlers): ldap managed group CRUDL APIs

* docs (domain): add LDAP accounts, auth-methods and managed groups (#2857)

* feature (ldap/cli) add ldap accounts and managed groups CRUDL commands (#2856)

* fix (handlers/authmethods): fix ldap authorized actions (#2892)

* feature (cli/ldap/authenticate): use primary auth method if none is provided (#2890)

* fix (auth/ldap): support setting the state attribute

* feature (cli/ldap/authenticate): use primary auth method if none is provided

* feature (wh/ldap) add tests for new ldap auth method and accounts (#2919)

* refactor (migrations/ldap): mv to correct directory

* chore: add copyright headers

* fix (api/authmethods/ldap): renumber new LdapAuthMethodAttributes field

* fix (auth/ldap): allow the auth method state to be updated (#2951)

* chore: update sdk and api versions for llb - this is tmp until merging

* tests (managed groups): add required errContains for new test
2023-03-15 11:40:40 -04:00

739 lines
27 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
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"
)
// 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,
},
fieldMaskPaths,
[]string{
StartTlsField,
InsecureTlsField,
DiscoverDnField,
AnonGroupSearchField,
EnableGroupsField,
UseTokenGroupsField,
},
)
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"))
}
addUrls, deleteUrls, 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"))
}
addCerts, deleteCerts, 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"))
}
addMaps, deleteMaps, 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"))
}
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)
}
addBindCred = bc
}
}
var filteredDbMask, filteredNullFields []string
for _, f := range dbMask {
switch f {
case
UrlsField,
CertificatesField,
AccountAttributeMapsField,
UserDnField, UserAttrField, UserFilterField,
GroupDnField, GroupAttrField, GroupFilterField,
ClientCertificateField, ClientCertificateKeyField,
BindDnField, BindPasswordField:
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:
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 {
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...)
}
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):
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
}