From 86192f75ebecea01ad686925a545818b12afe19f Mon Sep 17 00:00:00 2001 From: Jim Date: Wed, 15 Mar 2023 11:40:40 -0400 Subject: [PATCH] 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 --- Makefile | 1 + api/accounts/ldap_account_attributes.gen.go | 41 + api/accounts/option.gen.go | 24 + .../ldap_auth_method_attributes.gen.go | 59 + api/authmethods/option.gen.go | 504 +++++ .../ldap_managed_group_attributes.gen.go | 37 + api/managedgroups/option.gen.go | 24 + globals/prefixes.go | 8 + go.mod | 11 +- go.sum | 16 +- internal/api/genapi/input.go | 33 + internal/auth/additional_verification_test.go | 4 +- internal/auth/ldap/account.go | 146 ++ internal/auth/ldap/account_attribute_map.go | 151 ++ .../auth/ldap/account_attribute_map_test.go | 250 ++ internal/auth/ldap/account_test.go | 417 ++++ internal/auth/ldap/auth_method.go | 319 +++ internal/auth/ldap/auth_method_test.go | 594 +++++ internal/auth/ldap/bind_credential.go | 104 + internal/auth/ldap/bind_credential_test.go | 283 +++ internal/auth/ldap/certificate.go | 92 + internal/auth/ldap/certificate_test.go | 152 ++ internal/auth/ldap/certificate_utils.go | 62 + internal/auth/ldap/certificate_utils_test.go | 158 ++ internal/auth/ldap/client_certificate.go | 120 + internal/auth/ldap/client_certificate_test.go | 345 +++ internal/auth/ldap/group_entry_search_conf.go | 78 + .../auth/ldap/group_entry_search_conf_test.go | 180 ++ internal/auth/ldap/ids.go | 52 + internal/auth/ldap/managed_group.go | 106 + internal/auth/ldap/managed_group_test.go | 222 ++ internal/auth/ldap/options.go | 365 +++ internal/auth/ldap/options_test.go | 318 +++ internal/auth/ldap/repository.go | 55 + internal/auth/ldap/repository_account.go | 254 +++ internal/auth/ldap/repository_account_test.go | 1343 +++++++++++ .../ldap/repository_auth_method_create.go | 171 ++ .../repository_auth_method_create_test.go | 272 +++ .../ldap/repository_auth_method_delete.go | 62 + .../repository_auth_method_delete_test.go | 186 ++ .../auth/ldap/repository_auth_method_read.go | 248 ++ .../ldap/repository_auth_method_read_test.go | 357 +++ .../ldap/repository_auth_method_update.go | 738 ++++++ .../repository_auth_method_update_test.go | 1188 ++++++++++ internal/auth/ldap/repository_authenticate.go | 153 ++ .../auth/ldap/repository_authenticate_test.go | 298 +++ .../auth/ldap/repository_managed_group.go | 264 +++ .../ldap/repository_managed_group_members.go | 83 + .../repository_managed_group_members_test.go | 233 ++ .../ldap/repository_managed_group_test.go | 1227 ++++++++++ internal/auth/ldap/repository_test.go | 121 + internal/auth/ldap/rewrapping.go | 118 + internal/auth/ldap/rewrapping_test.go | 616 +++++ internal/auth/ldap/service_authenticate.go | 99 + .../auth/ldap/service_authenticate_test.go | 334 +++ internal/auth/ldap/state.go | 28 + internal/auth/ldap/state_test.go | 27 + internal/auth/ldap/store/ldap.pb.go | 2012 +++++++++++++++++ internal/auth/ldap/testing.go | 301 +++ internal/auth/ldap/testing_test.go | 121 + internal/auth/ldap/url.go | 78 + internal/auth/ldap/url_test.go | 152 ++ internal/auth/ldap/user_entry_search_conf.go | 78 + .../auth/ldap/user_entry_search_conf_test.go | 180 ++ internal/auth/testing.go | 58 + internal/cmd/commands.go | 41 + .../commands/accountscmd/ldap_accounts.gen.go | 275 +++ .../cmd/commands/accountscmd/ldap_funcs.go | 89 + .../cmd/commands/authenticate/authenticate.go | 10 +- internal/cmd/commands/authenticate/ldap.go | 197 ++ .../authmethodscmd/ldap_authmethods.gen.go | 280 +++ .../cmd/commands/authmethodscmd/ldap_funcs.go | 510 +++++ .../commands/managedgroupscmd/ldap_funcs.go | 86 + .../ldap_managedgroups.gen.go | 275 +++ internal/cmd/gencli/input.go | 43 + internal/daemon/controller/auth/auth.go | 15 +- internal/daemon/controller/common/common.go | 2 + internal/daemon/controller/controller.go | 5 + internal/daemon/controller/gateway.go | 3 +- internal/daemon/controller/handler.go | 6 +- .../handlers/accounts/account_service.go | 283 ++- .../handlers/accounts/account_service_test.go | 1311 +++++++++-- .../handlers/accounts/validate_test.go | 82 + .../authmethods/authmethod_service.go | 167 +- .../authmethods/authmethod_service_test.go | 440 +++- .../handlers/authmethods/authmethod_test.go | 37 + .../controller/handlers/authmethods/ldap.go | 366 +++ .../handlers/authmethods/ldap_test.go | 983 ++++++++ .../handlers/authmethods/oidc_test.go | 27 +- .../handlers/authmethods/password_test.go | 16 +- .../managed_groups/managed_group_service.go | 244 +- .../managed_group_service_test.go | 1093 ++++++++- .../handlers/managed_groups/validate_test.go | 70 +- .../handlers/scopes/scope_service.go | 4 +- .../targets/tcp/target_service_test.go | 5 + internal/daemon/controller/interceptor.go | 3 +- .../daemon/controller/interceptor_test.go | 2 +- internal/daemon/controller/listeners.go | 2 +- .../14/01_wh_user_dimension_oidc.up.sql | 1 + .../migrations/oss/postgres/65/01_ldap.up.sql | 644 ++++++ .../65/02_wh_user_dimension_ldap.up.sql | 93 + .../9/03_oidc_managed_group_member.up.sql | 1 + .../sqltest/initdb.d/03_widgets_persona.sql | 24 + .../db/sqltest/tests/account/ldap/account.sql | 38 + .../user_dimension/ldap_auth_new_session.sql | 84 + .../tests/wh/user_dimension_views/source.sql | 27 +- .../api/services/auth_method_service.pb.go | 604 ++--- .../api/resources/accounts/v1/account.proto | 41 + .../authmethods/v1/auth_method.proto | 268 +++ .../managedgroups/v1/managed_group.proto | 18 + .../api/services/v1/auth_method_service.proto | 9 + .../storage/auth/ldap/store/v1/ldap.proto | 592 +++++ internal/tests/api/accounts/account_test.go | 109 + .../tests/api/authmethods/authmethod_test.go | 60 + .../api/authmethods/classification_test.go | 95 + .../api/resources/accounts/account.pb.go | 268 ++- .../resources/authmethods/auth_method.pb.go | 842 +++++-- .../managedgroups/managed_group.pb.go | 169 +- .../docs/concepts/domain-model/accounts.mdx | 27 + .../concepts/domain-model/auth-methods.mdx | 92 + .../concepts/domain-model/managed-groups.mdx | 10 + 121 files changed, 26900 insertions(+), 919 deletions(-) create mode 100644 api/accounts/ldap_account_attributes.gen.go create mode 100644 api/authmethods/ldap_auth_method_attributes.gen.go create mode 100644 api/managedgroups/ldap_managed_group_attributes.gen.go create mode 100644 internal/auth/ldap/account.go create mode 100644 internal/auth/ldap/account_attribute_map.go create mode 100644 internal/auth/ldap/account_attribute_map_test.go create mode 100644 internal/auth/ldap/account_test.go create mode 100644 internal/auth/ldap/auth_method.go create mode 100644 internal/auth/ldap/auth_method_test.go create mode 100644 internal/auth/ldap/bind_credential.go create mode 100644 internal/auth/ldap/bind_credential_test.go create mode 100644 internal/auth/ldap/certificate.go create mode 100644 internal/auth/ldap/certificate_test.go create mode 100644 internal/auth/ldap/certificate_utils.go create mode 100644 internal/auth/ldap/certificate_utils_test.go create mode 100644 internal/auth/ldap/client_certificate.go create mode 100644 internal/auth/ldap/client_certificate_test.go create mode 100644 internal/auth/ldap/group_entry_search_conf.go create mode 100644 internal/auth/ldap/group_entry_search_conf_test.go create mode 100644 internal/auth/ldap/ids.go create mode 100644 internal/auth/ldap/managed_group.go create mode 100644 internal/auth/ldap/managed_group_test.go create mode 100644 internal/auth/ldap/options.go create mode 100644 internal/auth/ldap/options_test.go create mode 100644 internal/auth/ldap/repository.go create mode 100644 internal/auth/ldap/repository_account.go create mode 100644 internal/auth/ldap/repository_account_test.go create mode 100644 internal/auth/ldap/repository_auth_method_create.go create mode 100644 internal/auth/ldap/repository_auth_method_create_test.go create mode 100644 internal/auth/ldap/repository_auth_method_delete.go create mode 100644 internal/auth/ldap/repository_auth_method_delete_test.go create mode 100644 internal/auth/ldap/repository_auth_method_read.go create mode 100644 internal/auth/ldap/repository_auth_method_read_test.go create mode 100644 internal/auth/ldap/repository_auth_method_update.go create mode 100644 internal/auth/ldap/repository_auth_method_update_test.go create mode 100644 internal/auth/ldap/repository_authenticate.go create mode 100644 internal/auth/ldap/repository_authenticate_test.go create mode 100644 internal/auth/ldap/repository_managed_group.go create mode 100644 internal/auth/ldap/repository_managed_group_members.go create mode 100644 internal/auth/ldap/repository_managed_group_members_test.go create mode 100644 internal/auth/ldap/repository_managed_group_test.go create mode 100644 internal/auth/ldap/repository_test.go create mode 100644 internal/auth/ldap/rewrapping.go create mode 100644 internal/auth/ldap/rewrapping_test.go create mode 100644 internal/auth/ldap/service_authenticate.go create mode 100644 internal/auth/ldap/service_authenticate_test.go create mode 100644 internal/auth/ldap/state.go create mode 100644 internal/auth/ldap/state_test.go create mode 100644 internal/auth/ldap/store/ldap.pb.go create mode 100644 internal/auth/ldap/testing.go create mode 100644 internal/auth/ldap/testing_test.go create mode 100644 internal/auth/ldap/url.go create mode 100644 internal/auth/ldap/url_test.go create mode 100644 internal/auth/ldap/user_entry_search_conf.go create mode 100644 internal/auth/ldap/user_entry_search_conf_test.go create mode 100644 internal/auth/testing.go create mode 100644 internal/cmd/commands/accountscmd/ldap_accounts.gen.go create mode 100644 internal/cmd/commands/accountscmd/ldap_funcs.go create mode 100644 internal/cmd/commands/authenticate/ldap.go create mode 100644 internal/cmd/commands/authmethodscmd/ldap_authmethods.gen.go create mode 100644 internal/cmd/commands/authmethodscmd/ldap_funcs.go create mode 100644 internal/cmd/commands/managedgroupscmd/ldap_funcs.go create mode 100644 internal/cmd/commands/managedgroupscmd/ldap_managedgroups.gen.go create mode 100644 internal/daemon/controller/handlers/authmethods/ldap.go create mode 100644 internal/daemon/controller/handlers/authmethods/ldap_test.go create mode 100644 internal/db/schema/migrations/oss/postgres/65/01_ldap.up.sql create mode 100644 internal/db/schema/migrations/oss/postgres/65/02_wh_user_dimension_ldap.up.sql create mode 100644 internal/db/sqltest/tests/account/ldap/account.sql create mode 100644 internal/db/sqltest/tests/wh/user_dimension/ldap_auth_new_session.sql create mode 100644 internal/proto/controller/storage/auth/ldap/store/v1/ldap.proto diff --git a/Makefile b/Makefile index c06399ba51..1ada4969ab 100644 --- a/Makefile +++ b/Makefile @@ -191,6 +191,7 @@ protobuild: @protoc-go-inject-tag -input=./internal/credential/vault/store/vault.pb.go @protoc-go-inject-tag -input=./internal/credential/static/store/static.pb.go @protoc-go-inject-tag -input=./internal/kms/store/audit_key.pb.go + @protoc-go-inject-tag -input=./internal/auth/ldap/store/ldap.pb.go # inject classification tags (see: https://github.com/hashicorp/go-eventlogger/tree/main/filters/encrypt) @protoc-go-inject-tag -input=./internal/gen/controller/api/services/auth_method_service.pb.go diff --git a/api/accounts/ldap_account_attributes.gen.go b/api/accounts/ldap_account_attributes.gen.go new file mode 100644 index 0000000000..d7628ff9a0 --- /dev/null +++ b/api/accounts/ldap_account_attributes.gen.go @@ -0,0 +1,41 @@ +// Code generated by "make api"; DO NOT EDIT. +package accounts + +import ( + "fmt" + + "github.com/mitchellh/mapstructure" +) + +type LdapAccountAttributes struct { + LoginName string `json:"login_name,omitempty"` + FullName string `json:"full_name,omitempty"` + Email string `json:"email,omitempty"` + Dn string `json:"dn,omitempty"` + MemberOfGroups []string `json:"member_of_groups,omitempty"` +} + +func AttributesMapToLdapAccountAttributes(in map[string]interface{}) (*LdapAccountAttributes, error) { + if in == nil { + return nil, fmt.Errorf("nil input map") + } + var out LdapAccountAttributes + dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + Result: &out, + TagName: "json", + }) + if err != nil { + return nil, fmt.Errorf("error creating mapstructure decoder: %w", err) + } + if err := dec.Decode(in); err != nil { + return nil, fmt.Errorf("error decoding: %w", err) + } + return &out, nil +} + +func (pt *Account) GetLdapAccountAttributes() (*LdapAccountAttributes, error) { + if pt.Type != "ldap" { + return nil, fmt.Errorf("asked to fetch %s-type attributes but account is of type %s", "ldap", pt.Type) + } + return AttributesMapToLdapAccountAttributes(pt.Attributes) +} diff --git a/api/accounts/option.gen.go b/api/accounts/option.gen.go index 0ab8ccc17f..4a1d66bc96 100644 --- a/api/accounts/option.gen.go +++ b/api/accounts/option.gen.go @@ -125,6 +125,30 @@ func DefaultOidcAccountIssuer() Option { } } +func WithLdapAccountLoginName(inLoginName string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["login_name"] = inLoginName + o.postMap["attributes"] = val + } +} + +func DefaultLdapAccountLoginName() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["login_name"] = nil + o.postMap["attributes"] = val + } +} + func WithPasswordAccountLoginName(inLoginName string) Option { return func(o *options) { raw, ok := o.postMap["attributes"] diff --git a/api/authmethods/ldap_auth_method_attributes.gen.go b/api/authmethods/ldap_auth_method_attributes.gen.go new file mode 100644 index 0000000000..ffe8900a6d --- /dev/null +++ b/api/authmethods/ldap_auth_method_attributes.gen.go @@ -0,0 +1,59 @@ +// Code generated by "make api"; DO NOT EDIT. +package authmethods + +import ( + "fmt" + + "github.com/mitchellh/mapstructure" +) + +type LdapAuthMethodAttributes struct { + State string `json:"state,omitempty"` + StartTls bool `json:"start_tls,omitempty"` + InsecureTls bool `json:"insecure_tls,omitempty"` + DiscoverDn bool `json:"discover_dn,omitempty"` + AnonGroupSearch bool `json:"anon_group_search,omitempty"` + UpnDomain string `json:"upn_domain,omitempty"` + Urls []string `json:"urls,omitempty"` + UserDn string `json:"user_dn,omitempty"` + UserAttr string `json:"user_attr,omitempty"` + UserFilter string `json:"user_filter,omitempty"` + EnableGroups bool `json:"enable_groups,omitempty"` + GroupDn string `json:"group_dn,omitempty"` + GroupAttr string `json:"group_attr,omitempty"` + GroupFilter string `json:"group_filter,omitempty"` + Certificates []string `json:"certificates,omitempty"` + ClientCertificate string `json:"client_certificate,omitempty"` + ClientCertificateKey string `json:"client_certificate_key,omitempty"` + ClientCertificateKeyHmac string `json:"client_certificate_key_hmac,omitempty"` + BindDn string `json:"bind_dn,omitempty"` + BindPassword string `json:"bind_password,omitempty"` + BindPasswordHmac string `json:"bind_password_hmac,omitempty"` + UseTokenGroups bool `json:"use_token_groups,omitempty"` + AccountAttributeMaps []string `json:"account_attribute_maps,omitempty"` +} + +func AttributesMapToLdapAuthMethodAttributes(in map[string]interface{}) (*LdapAuthMethodAttributes, error) { + if in == nil { + return nil, fmt.Errorf("nil input map") + } + var out LdapAuthMethodAttributes + dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + Result: &out, + TagName: "json", + }) + if err != nil { + return nil, fmt.Errorf("error creating mapstructure decoder: %w", err) + } + if err := dec.Decode(in); err != nil { + return nil, fmt.Errorf("error decoding: %w", err) + } + return &out, nil +} + +func (pt *AuthMethod) GetLdapAuthMethodAttributes() (*LdapAuthMethodAttributes, error) { + if pt.Type != "ldap" { + return nil, fmt.Errorf("asked to fetch %s-type attributes but auth-method is of type %s", "ldap", pt.Type) + } + return AttributesMapToLdapAuthMethodAttributes(pt.Attributes) +} diff --git a/api/authmethods/option.gen.go b/api/authmethods/option.gen.go index 9ada6e4c78..4f793f7c53 100644 --- a/api/authmethods/option.gen.go +++ b/api/authmethods/option.gen.go @@ -90,6 +90,30 @@ func WithRecursive(recurse bool) Option { } } +func WithLdapAuthMethodAccountAttributeMaps(inAccountAttributeMaps []string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["account_attribute_maps"] = inAccountAttributeMaps + o.postMap["attributes"] = val + } +} + +func DefaultLdapAuthMethodAccountAttributeMaps() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["account_attribute_maps"] = nil + o.postMap["attributes"] = val + } +} + func WithOidcAuthMethodAccountClaimMaps(inAccountClaimMaps []string) Option { return func(o *options) { raw, ok := o.postMap["attributes"] @@ -138,6 +162,30 @@ func DefaultOidcAuthMethodAllowedAudiences() Option { } } +func WithLdapAuthMethodAnonGroupSearch(inAnonGroupSearch bool) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["anon_group_search"] = inAnonGroupSearch + o.postMap["attributes"] = val + } +} + +func DefaultLdapAuthMethodAnonGroupSearch() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["anon_group_search"] = nil + o.postMap["attributes"] = val + } +} + func WithOidcAuthMethodApiUrlPrefix(inApiUrlPrefix string) Option { return func(o *options) { raw, ok := o.postMap["attributes"] @@ -174,6 +222,78 @@ func DefaultAttributes() Option { } } +func WithLdapAuthMethodBindDn(inBindDn string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["bind_dn"] = inBindDn + o.postMap["attributes"] = val + } +} + +func DefaultLdapAuthMethodBindDn() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["bind_dn"] = nil + o.postMap["attributes"] = val + } +} + +func WithLdapAuthMethodBindPassword(inBindPassword string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["bind_password"] = inBindPassword + o.postMap["attributes"] = val + } +} + +func DefaultLdapAuthMethodBindPassword() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["bind_password"] = nil + o.postMap["attributes"] = val + } +} + +func WithLdapAuthMethodCertificates(inCertificates []string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["certificates"] = inCertificates + o.postMap["attributes"] = val + } +} + +func DefaultLdapAuthMethodCertificates() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["certificates"] = nil + o.postMap["attributes"] = val + } +} + func WithOidcAuthMethodClaimsScopes(inClaimsScopes []string) Option { return func(o *options) { raw, ok := o.postMap["attributes"] @@ -198,6 +318,54 @@ func DefaultOidcAuthMethodClaimsScopes() Option { } } +func WithLdapAuthMethodClientCertificate(inClientCertificate string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["client_certificate"] = inClientCertificate + o.postMap["attributes"] = val + } +} + +func DefaultLdapAuthMethodClientCertificate() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["client_certificate"] = nil + o.postMap["attributes"] = val + } +} + +func WithLdapAuthMethodClientCertificateKey(inClientCertificateKey string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["client_certificate_key"] = inClientCertificateKey + o.postMap["attributes"] = val + } +} + +func DefaultLdapAuthMethodClientCertificateKey() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["client_certificate_key"] = nil + o.postMap["attributes"] = val + } +} + func WithOidcAuthMethodClientId(inClientId string) Option { return func(o *options) { raw, ok := o.postMap["attributes"] @@ -282,6 +450,30 @@ func DefaultOidcAuthMethodDisableDiscoveredConfigValidation() Option { } } +func WithLdapAuthMethodDiscoverDn(inDiscoverDn bool) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["discover_dn"] = inDiscoverDn + o.postMap["attributes"] = val + } +} + +func DefaultLdapAuthMethodDiscoverDn() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["discover_dn"] = nil + o.postMap["attributes"] = val + } +} + func WithOidcAuthMethodDryRun(inDryRun bool) Option { return func(o *options) { raw, ok := o.postMap["attributes"] @@ -306,6 +498,102 @@ func DefaultOidcAuthMethodDryRun() Option { } } +func WithLdapAuthMethodEnableGroups(inEnableGroups bool) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["enable_groups"] = inEnableGroups + o.postMap["attributes"] = val + } +} + +func DefaultLdapAuthMethodEnableGroups() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["enable_groups"] = nil + o.postMap["attributes"] = val + } +} + +func WithLdapAuthMethodGroupAttr(inGroupAttr string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["group_attr"] = inGroupAttr + o.postMap["attributes"] = val + } +} + +func DefaultLdapAuthMethodGroupAttr() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["group_attr"] = nil + o.postMap["attributes"] = val + } +} + +func WithLdapAuthMethodGroupDn(inGroupDn string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["group_dn"] = inGroupDn + o.postMap["attributes"] = val + } +} + +func DefaultLdapAuthMethodGroupDn() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["group_dn"] = nil + o.postMap["attributes"] = val + } +} + +func WithLdapAuthMethodGroupFilter(inGroupFilter string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["group_filter"] = inGroupFilter + o.postMap["attributes"] = val + } +} + +func DefaultLdapAuthMethodGroupFilter() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["group_filter"] = nil + o.postMap["attributes"] = val + } +} + func WithOidcAuthMethodIdpCaCerts(inIdpCaCerts []string) Option { return func(o *options) { raw, ok := o.postMap["attributes"] @@ -330,6 +618,30 @@ func DefaultOidcAuthMethodIdpCaCerts() Option { } } +func WithLdapAuthMethodInsecureTls(inInsecureTls bool) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["insecure_tls"] = inInsecureTls + o.postMap["attributes"] = val + } +} + +func DefaultLdapAuthMethodInsecureTls() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["insecure_tls"] = nil + o.postMap["attributes"] = val + } +} + func WithOidcAuthMethodIssuer(inIssuer string) Option { return func(o *options) { raw, ok := o.postMap["attributes"] @@ -461,3 +773,195 @@ func DefaultOidcAuthMethodSigningAlgorithms() Option { o.postMap["attributes"] = val } } + +func WithLdapAuthMethodStartTls(inStartTls bool) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["start_tls"] = inStartTls + o.postMap["attributes"] = val + } +} + +func DefaultLdapAuthMethodStartTls() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["start_tls"] = nil + o.postMap["attributes"] = val + } +} + +func WithLdapAuthMethodState(inState string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["state"] = inState + o.postMap["attributes"] = val + } +} + +func DefaultLdapAuthMethodState() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["state"] = nil + o.postMap["attributes"] = val + } +} + +func WithLdapAuthMethodUpnDomain(inUpnDomain string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["upn_domain"] = inUpnDomain + o.postMap["attributes"] = val + } +} + +func DefaultLdapAuthMethodUpnDomain() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["upn_domain"] = nil + o.postMap["attributes"] = val + } +} + +func WithLdapAuthMethodUrls(inUrls []string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["urls"] = inUrls + o.postMap["attributes"] = val + } +} + +func DefaultLdapAuthMethodUrls() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["urls"] = nil + o.postMap["attributes"] = val + } +} + +func WithLdapAuthMethodUseTokenGroups(inUseTokenGroups bool) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["use_token_groups"] = inUseTokenGroups + o.postMap["attributes"] = val + } +} + +func DefaultLdapAuthMethodUseTokenGroups() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["use_token_groups"] = nil + o.postMap["attributes"] = val + } +} + +func WithLdapAuthMethodUserAttr(inUserAttr string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["user_attr"] = inUserAttr + o.postMap["attributes"] = val + } +} + +func DefaultLdapAuthMethodUserAttr() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["user_attr"] = nil + o.postMap["attributes"] = val + } +} + +func WithLdapAuthMethodUserDn(inUserDn string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["user_dn"] = inUserDn + o.postMap["attributes"] = val + } +} + +func DefaultLdapAuthMethodUserDn() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["user_dn"] = nil + o.postMap["attributes"] = val + } +} + +func WithLdapAuthMethodUserFilter(inUserFilter string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["user_filter"] = inUserFilter + o.postMap["attributes"] = val + } +} + +func DefaultLdapAuthMethodUserFilter() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["user_filter"] = nil + o.postMap["attributes"] = val + } +} diff --git a/api/managedgroups/ldap_managed_group_attributes.gen.go b/api/managedgroups/ldap_managed_group_attributes.gen.go new file mode 100644 index 0000000000..3eeef101a3 --- /dev/null +++ b/api/managedgroups/ldap_managed_group_attributes.gen.go @@ -0,0 +1,37 @@ +// Code generated by "make api"; DO NOT EDIT. +package managedgroups + +import ( + "fmt" + + "github.com/mitchellh/mapstructure" +) + +type LdapManagedGroupAttributes struct { + GroupNames []string `json:"group_names,omitempty"` +} + +func AttributesMapToLdapManagedGroupAttributes(in map[string]interface{}) (*LdapManagedGroupAttributes, error) { + if in == nil { + return nil, fmt.Errorf("nil input map") + } + var out LdapManagedGroupAttributes + dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + Result: &out, + TagName: "json", + }) + if err != nil { + return nil, fmt.Errorf("error creating mapstructure decoder: %w", err) + } + if err := dec.Decode(in); err != nil { + return nil, fmt.Errorf("error decoding: %w", err) + } + return &out, nil +} + +func (pt *ManagedGroup) GetLdapManagedGroupAttributes() (*LdapManagedGroupAttributes, error) { + if pt.Type != "ldap" { + return nil, fmt.Errorf("asked to fetch %s-type attributes but managed-group is of type %s", "ldap", pt.Type) + } + return AttributesMapToLdapManagedGroupAttributes(pt.Attributes) +} diff --git a/api/managedgroups/option.gen.go b/api/managedgroups/option.gen.go index f91fc0fba2..da6ab607d5 100644 --- a/api/managedgroups/option.gen.go +++ b/api/managedgroups/option.gen.go @@ -113,6 +113,30 @@ func WithOidcManagedGroupFilter(inFilter string) Option { } } +func WithLdapManagedGroupGroupNames(inGroupNames []string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["group_names"] = inGroupNames + o.postMap["attributes"] = val + } +} + +func DefaultLdapManagedGroupGroupNames() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["group_names"] = nil + o.postMap["attributes"] = val + } +} + func WithName(inName string) Option { return func(o *options) { o.postMap["name"] = inName diff --git a/globals/prefixes.go b/globals/prefixes.go index ff70d8bcc5..2efa9e485f 100644 --- a/globals/prefixes.go +++ b/globals/prefixes.go @@ -29,6 +29,14 @@ const ( // ids OidcManagedGroupPrefix = "mgoidc" + // LdapManagedGroupPrefix defines the prefix for ManagedGroup public ids + // from the LDAP auth method. + LdapManagedGroupPrefix = "mgldap" + // AuthMethodPrefix defines the prefix for AuthMethod public ids. + LdapAuthMethodPrefix = "amldap" + // AccountPrefix defines the prefix for Account public ids. + LdapAccountPrefix = "acctldap" + // ProjectPrefix is the prefix for project scopes ProjectPrefix = "p" // OrgPrefix is the prefix for org scopes diff --git a/go.mod b/go.mod index 268a4bc0da..133423baf7 100644 --- a/go.mod +++ b/go.mod @@ -20,8 +20,8 @@ require ( github.com/google/go-cmp v0.5.9 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.2 - github.com/hashicorp/boundary/api v0.0.34 - github.com/hashicorp/boundary/sdk v0.0.30 + github.com/hashicorp/boundary/api v0.0.34-0.20230215183053-218de6441d53 + github.com/hashicorp/boundary/sdk v0.0.30-0.20230215183053-218de6441d53 github.com/hashicorp/cap v0.1.1 github.com/hashicorp/dawdle v0.4.0 github.com/hashicorp/dbassert v0.0.0-20210708202608-ecf920cf1ed8 @@ -93,12 +93,14 @@ require ( github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/cenkalti/backoff/v4 v4.1.3 github.com/creack/pty v1.1.11 + github.com/hashicorp/cap/ldap v0.0.0-20230123181313-9c0fb924b0d9 github.com/hashicorp/go-kms-wrapping/extras/kms/v2 v2.0.0-20221122211539-47c893099f13 github.com/hashicorp/go-version v1.3.0 github.com/hashicorp/nodeenrollment v0.1.18 + github.com/jimlambrt/gldap v0.1.2 github.com/kelseyhightower/envconfig v1.4.0 github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a - golang.org/x/exp v0.0.0-20220921164117-439092de6870 + golang.org/x/exp v0.0.0-20221230185412-738e83a70c30 golang.org/x/net v0.7.0 ) @@ -106,6 +108,7 @@ require ( github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect github.com/AlecAivazis/survey/v2 v2.2.9 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.2.0 // indirect github.com/Masterminds/sprig/v3 v3.2.3 // indirect @@ -128,6 +131,8 @@ require ( github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect github.com/dvsekhvalnov/jose2go v1.5.0 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect + github.com/go-ldap/ldap/v3 v3.4.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/glog v1.0.0 // indirect github.com/golang/snappy v0.0.4 // indirect diff --git a/go.sum b/go.sum index 251c047bd4..3ec4648b5c 100644 --- a/go.sum +++ b/go.sum @@ -66,6 +66,8 @@ github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935 github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e h1:ZU22z/2YRFLyf/P4ZwUYSdNCWsMEI0VeyrFoI2rAhJQ= +github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= @@ -434,6 +436,8 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= +github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= @@ -448,6 +452,8 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= +github.com/go-ldap/ldap/v3 v3.4.3 h1:JCKUtJPIcyOuG7ctGabLKMgIlKnGumD/iGjuWeEruDI= +github.com/go-ldap/ldap/v3 v3.4.3/go.mod h1:7LdHfVt6iIOESVEe3Bs4Jp2sHEKgDeduAhgM1/f9qmo= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= @@ -651,6 +657,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.2/go.mod h1:ZbS3MZTZq/apAfAEHGoB github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hashicorp/cap v0.1.1 h1:GjO4+9+H0wv/89YoEsxeVc2jIizL19r5v5l2lpaH8Kg= github.com/hashicorp/cap v0.1.1/go.mod h1:VfBvK2ULRyqsuqAnjgZl7HJ7/CGMC7ro4H5eXiZuun8= +github.com/hashicorp/cap/ldap v0.0.0-20230123181313-9c0fb924b0d9 h1:hTJWCzuFRrGnPjPWbb+fxBQD50MKswdQnwpTH4A7YDE= +github.com/hashicorp/cap/ldap v0.0.0-20230123181313-9c0fb924b0d9/go.mod h1:raCUTLtdFrAMx/rsQNeYoWDrsfFcqQuTK1vJ5tYd3qA= github.com/hashicorp/dawdle v0.4.0 h1:nAE+aCoAmVulgCPPPrp6oGOykjPhxEe4VUNms/mTKeA= github.com/hashicorp/dawdle v0.4.0/go.mod h1:gOBY8rFXpW4Rw39up1FIacw/1pG/fO779LfchCY3KUw= github.com/hashicorp/dbassert v0.0.0-20210708202608-ecf920cf1ed8 h1:/3iubtqEdLgdXB6ue+WAKvbcpn313jjL5mQvU5ByymE= @@ -860,6 +868,8 @@ github.com/jefferai/keyring v1.1.7-0.20220316160357-58a74bb55891/go.mod h1:iwmrB github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= github.com/jhump/protoreflect v1.9.1-0.20210817181203-db1a327a393e h1:Yb4fEGk+GtBSNuvy5rs0ZJt/jtopc/z9azQaj3xbies= +github.com/jimlambrt/gldap v0.1.2 h1:Xprug+i9WdvdQd8u2bi05JVbllZ+SHhNu4alDY38+Kw= +github.com/jimlambrt/gldap v0.1.2/go.mod h1:sKo9VprcJwZRj7OoE7p8YLaPEeNxw3WIEY42NS/iV7E= github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q= github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= @@ -1400,6 +1410,7 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= @@ -1417,8 +1428,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20220921164117-439092de6870 h1:j8b6j9gzSigH28O5SjSpQSSh9lFd6f5D/q0aHjNTulc= -golang.org/x/exp v0.0.0-20220921164117-439092de6870/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/exp v0.0.0-20221230185412-738e83a70c30 h1:m9O6OTJ627iFnN2JIWfdqlZCzneRO6EEBsHXI25P8ws= +golang.org/x/exp v0.0.0-20221230185412-738e83a70c30/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -1515,6 +1526,7 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211013171255-e13a2654a71e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= diff --git a/internal/api/genapi/input.go b/internal/api/genapi/input.go index 390f589c3a..cb3f3c24c1 100644 --- a/internal/api/genapi/input.go +++ b/internal/api/genapi/input.go @@ -320,6 +320,15 @@ var inputStructs = []*structInfo{ mapstructureConversionTemplate, }, }, + { + inProto: &authmethods.LdapAuthMethodAttributes{}, + outFile: "authmethods/ldap_auth_method_attributes.gen.go", + subtypeName: "LdapAuthMethod", + parentTypeName: "AuthMethod", + templates: []*template.Template{ + mapstructureConversionTemplate, + }, + }, { inProto: &authmethods.OidcAuthMethodAttributes{}, outFile: "authmethods/oidc_auth_method_attributes.gen.go", @@ -379,6 +388,15 @@ var inputStructs = []*structInfo{ mapstructureConversionTemplate, }, }, + { + inProto: &accounts.LdapAccountAttributes{}, + outFile: "accounts/ldap_account_attributes.gen.go", + subtypeName: "LdapAccount", + parentTypeName: "Account", + templates: []*template.Template{ + mapstructureConversionTemplate, + }, + }, { inProto: &accounts.OidcAccountAttributes{}, outFile: "accounts/oidc_account_attributes.gen.go", @@ -420,6 +438,21 @@ var inputStructs = []*structInfo{ mapstructureConversionTemplate, }, }, + { + inProto: &managedgroups.LdapManagedGroupAttributes{}, + outFile: "managedgroups/ldap_managed_group_attributes.gen.go", + subtypeName: "LdapManagedGroup", + fieldOverrides: []fieldInfo{ + { + Name: "Filter", + SkipDefault: true, + }, + }, + parentTypeName: "ManagedGroup", + templates: []*template.Template{ + mapstructureConversionTemplate, + }, + }, { inProto: &managedgroups.ManagedGroup{}, outFile: "managedgroups/managedgroups.gen.go", diff --git a/internal/auth/additional_verification_test.go b/internal/auth/additional_verification_test.go index 8f5223b638..98c126da90 100644 --- a/internal/auth/additional_verification_test.go +++ b/internal/auth/additional_verification_test.go @@ -219,7 +219,9 @@ func TestSelfReadingDifferentOutputFields(t *testing.T) { tc.Controller().PasswordAuthRepoFn, tc.Controller().OidcRepoFn, tc.Controller().IamRepoFn, - tc.Controller().AuthTokenRepoFn) + tc.Controller().AuthTokenRepoFn, + tc.Controller().LdapRepoFn, + ) require.NoError(t, err) // Create two auth tokens belonging to different users in the org. Each will diff --git a/internal/auth/ldap/account.go b/internal/auth/ldap/account.go new file mode 100644 index 0000000000..e692aaef93 --- /dev/null +++ b/internal/auth/ldap/account.go @@ -0,0 +1,146 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "strings" + + "github.com/hashicorp/boundary/internal/auth" + "github.com/hashicorp/boundary/internal/auth/ldap/store" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/oplog" + "google.golang.org/protobuf/proto" +) + +// accountTableName defines the default table name for an Account +const accountTableName = "auth_ldap_account" + +// Account contains an ldap auth account. It is assigned to an ldap AuthMethod +// and updates/deletes to that AuthMethod are cascaded to its Accounts. +type Account struct { + *store.Account + tableName string +} + +// make sure ldap.Account implements the auth.Account interface +var _ auth.Account = (*Account)(nil) + +// NewAccount creates a new in memory Account assigned to ldap AuthMethod. +// WithFullName, WithEmail, WithDn, WithName and WithDescription are the only +// valid options. All other options are ignored. +func NewAccount(ctx context.Context, scopeId, authMethodId, loginName string, opt ...Option) (*Account, error) { + const op = "ldap.NewAccount" + switch { + case scopeId == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope id") + case authMethodId == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id") + case loginName == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing login name") + } + opts, err := getOpts(opt...) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + a := &Account{ + Account: &store.Account{ + ScopeId: scopeId, + AuthMethodId: authMethodId, + LoginName: loginName, + Dn: opts.withDn, + Name: opts.withName, + Description: opts.withDescription, + FullName: opts.withFullName, + Email: opts.withEmail, + MemberOfGroups: opts.withMemberOfGroups, + }, + } + if err := a.validate(ctx, op); err != nil { + return nil, err // intentionally not wrapped. + } + return a, nil +} + +// validate the Account. On success, it will return nil. +func (a *Account) validate(ctx context.Context, caller errors.Op) error { + const op = "ldap.(Account).validate" + switch { + case caller == "": + return errors.New(ctx, errors.InvalidParameter, op, "missing caller") + case a.ScopeId == "": + return errors.New(ctx, errors.InvalidParameter, caller, "missing scope id") + case a.AuthMethodId == "": + return errors.New(ctx, errors.InvalidParameter, caller, "missing auth method id") + case a.LoginName == "": + return errors.New(ctx, errors.InvalidParameter, caller, "missing login name") + case strings.ToLower(a.LoginName) != a.LoginName: + return errors.New(ctx, errors.InvalidParameter, op, "login name must be lower case") + case a.Email != "" && len(a.Email) > 320: + return errors.New(ctx, errors.InvalidParameter, caller, "email address is too long") + case a.FullName != "" && len(a.FullName) > 512: + return errors.New(ctx, errors.InvalidParameter, caller, "full name is too long") + default: + return nil + } +} + +// AllocAccount makes an empty one in memory +func AllocAccount() *Account { + return &Account{ + Account: &store.Account{}, + } +} + +// clone an Account. +func (a *Account) clone() *Account { + cp := proto.Clone(a.Account) + return &Account{ + Account: cp.(*store.Account), + } +} + +// TableName returns the table name. +func (a *Account) TableName() string { + if a.tableName != "" { + return a.tableName + } + return accountTableName +} + +// SetTableName sets the table name. +func (a *Account) SetTableName(n string) { + a.tableName = n +} + +// GetSubject returns the subject, which will always be empty as this type +// doesn't currently support subject. +func (a *Account) GetSubject() string { + return "" +} + +// oplog will create oplog metadata for the Account. +func (a *Account) oplog(ctx context.Context, opType oplog.OpType) (oplog.Metadata, error) { + const op = "ldap.(Account).oplog" + switch { + case a == nil: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing account") + case opType == oplog.OpType_OP_TYPE_UNSPECIFIED: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing op type") + case a.PublicId == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing public id") + case a.ScopeId == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope id") + case a.AuthMethodId == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id") + } + metadata := oplog.Metadata{ + "resource-public-id": []string{a.PublicId}, + "resource-type": []string{"ldap account"}, + "op-type": []string{opType.String()}, + "scope-id": []string{a.ScopeId}, + "auth-method-id": []string{a.AuthMethodId}, + } + return metadata, nil +} diff --git a/internal/auth/ldap/account_attribute_map.go b/internal/auth/ldap/account_attribute_map.go new file mode 100644 index 0000000000..480e8f7d66 --- /dev/null +++ b/internal/auth/ldap/account_attribute_map.go @@ -0,0 +1,151 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "fmt" + "sort" + "strings" + + "github.com/hashicorp/boundary/internal/auth/ldap/store" + "github.com/hashicorp/boundary/internal/errors" + "golang.org/x/exp/slices" + "google.golang.org/protobuf/proto" +) + +const ( + acctAttributeMapTableName = "auth_ldap_account_attribute_map" +) + +// AccountToAttribute defines a type for: to account attributes +type AccountToAttribute string + +const ( + // ToEmailAttribute defines the valid email attribute name + ToEmailAttribute AccountToAttribute = "email" + // ToFullNameAttribute defines the valid full name attribute name + ToFullNameAttribute AccountToAttribute = "fullName" +) + +// ConvertToAccountToAttribute will convert a string to an AccountToAttribute. +// Useful within the ldap package and service packages which wish to +// convert/validate a string into an AccountToAttribute +func ConvertToAccountToAttribute(ctx context.Context, s string) (AccountToAttribute, error) { + const op = "ldap.ConvertToAccountToAttribute" + switch { + case strings.EqualFold(s, string(ToEmailAttribute)): + return ToEmailAttribute, nil + case strings.EqualFold(s, string(ToFullNameAttribute)): + return ToFullNameAttribute, nil + default: + return "", errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("%q is not a valid ToAccountAttribute value (%q, %q)", s, ToEmailAttribute, ToFullNameAttribute)) + } +} + +// AccountAttributeMap defines optional from/to account attribute maps. +type AccountAttributeMap struct { + *store.AccountAttributeMap + tableName string +} + +// NewAccountAttributeMap creates a new one in memory +func NewAccountAttributeMap(ctx context.Context, authMethodId, fromAttribute string, toAttribute AccountToAttribute) (*AccountAttributeMap, error) { + const op = "ldap.NewAccountAttributeMap" + aam := &AccountAttributeMap{ + AccountAttributeMap: &store.AccountAttributeMap{ + LdapMethodId: authMethodId, + FromAttribute: fromAttribute, + ToAttribute: string(toAttribute), + }, + } + if err := aam.validate(ctx, op); err != nil { + return nil, err + } + return aam, nil +} + +// validate the AccountClaimMap. On success, it will return nil. +func (aam *AccountAttributeMap) validate(ctx context.Context, caller errors.Op) error { + if aam.LdapMethodId == "" { + return errors.New(ctx, errors.InvalidParameter, caller, "missing ldap auth method id") + } + if aam.FromAttribute == "" { + return errors.New(ctx, errors.InvalidParameter, caller, "missing from attribute") + } + if _, err := ConvertToAccountToAttribute(ctx, aam.ToAttribute); err != nil { + return errors.Wrap(ctx, err, caller) + } + return nil +} + +// AllocAccountAttributeMap makes an empty one in memory +func AllocAccountAttributeMap() AccountAttributeMap { + return AccountAttributeMap{ + AccountAttributeMap: &store.AccountAttributeMap{}, + } +} + +// clone a AccountAttributeMap +func (aam *AccountAttributeMap) clone() *AccountAttributeMap { + cp := proto.Clone(aam.AccountAttributeMap) + return &AccountAttributeMap{ + AccountAttributeMap: cp.(*store.AccountAttributeMap), + } +} + +// TableName returns the table name. +func (aam *AccountAttributeMap) TableName() string { + if aam.tableName != "" { + return aam.tableName + } + return acctAttributeMapTableName +} + +// SetTableName sets the table name. +func (aam *AccountAttributeMap) SetTableName(n string) { + aam.tableName = n +} + +// AttributeMap defines the To and From of an ldap attribute map +type AttributeMap struct { + To string + From string +} + +// ParseAccountAttributeMaps will parse the inbound attribute maps +func ParseAccountAttributeMaps(ctx context.Context, m ...string) ([]AttributeMap, error) { + const op = "ldap.ParseAccountAttributeMaps" + + am := make([]AttributeMap, 0, len(m)) + for _, s := range m { + // Split into key/value which maps From/To + parts := strings.SplitN(s, "=", 2) + if len(parts) != 2 { + return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("error parsing attribute map %q: format must be key=value", s)) + } + from, to := parts[0], parts[1] + toAttr, err := ConvertToAccountToAttribute(ctx, to) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + found := slices.ContainsFunc(am, func(m AttributeMap) bool { + if m.To == to { + return true + } + return false + }) + if found { + return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("duplicate map for %q attribute", toAttr)) + } + am = append(am, AttributeMap{ + To: string(to), + From: from, + }) + } + sort.Slice(am, func(i, j int) bool { + return am[i].From < am[j].From + }) + return am, nil +} diff --git a/internal/auth/ldap/account_attribute_map_test.go b/internal/auth/ldap/account_attribute_map_test.go new file mode 100644 index 0000000000..00977f623c --- /dev/null +++ b/internal/auth/ldap/account_attribute_map_test.go @@ -0,0 +1,250 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/boundary/internal/auth/ldap/store" + "github.com/hashicorp/boundary/internal/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestNewAccountAttributeMap(t *testing.T) { + t.Parallel() + testCtx := context.Background() + tests := []struct { + name string + ctx context.Context + authMethodId string + from string + to AccountToAttribute + want *AccountAttributeMap + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "success", + ctx: testCtx, + authMethodId: "test-auth-method-id", + from: "mail", + to: ToEmailAttribute, + want: &AccountAttributeMap{ + AccountAttributeMap: &store.AccountAttributeMap{ + LdapMethodId: "test-auth-method-id", + FromAttribute: "mail", + ToAttribute: string(ToEmailAttribute), + }, + }, + }, + { + name: "missing-auth-method-id", + ctx: testCtx, + from: "mail", + to: ToEmailAttribute, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing ldap auth method id", + }, + { + name: "missing-from", + ctx: testCtx, + authMethodId: "test-auth-method-id", + to: ToEmailAttribute, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing from attribute", + }, + { + name: "missing-to", + ctx: testCtx, + authMethodId: "test-auth-method-id", + from: "mail", + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "\"\" is not a valid ToAccountAttribute value", + }, + { + name: "invalid-to", + ctx: testCtx, + authMethodId: "test-auth-method-id", + from: "mail", + to: "invalid-to", + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "\"invalid-to\" is not a valid ToAccountAttribute value", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := NewAccountAttributeMap(tc.ctx, tc.authMethodId, tc.from, tc.to) + if tc.wantErrMatch != nil { + require.Error(err) + assert.Empty(got) + assert.Truef(errors.Match(tc.wantErrMatch, err), "unexpected error: %q", err.Error()) + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + require.NotNil(got) + assert.Equal(tc.want, got) + }) + } +} + +func TestAccountAttributeMap_Clone(t *testing.T) { + t.Parallel() + testCtx := context.Background() + t.Run("valid", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + orig, err := NewAccountAttributeMap(testCtx, "test-scope-id", "displayName", ToFullNameAttribute) + require.NoError(err) + cp := orig.clone() + assert.True(proto.Equal(cp.AccountAttributeMap, orig.AccountAttributeMap)) + }) + t.Run("not-equal", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + orig, err := NewAccountAttributeMap(testCtx, "test-scope-id", "displayName", ToFullNameAttribute) + require.NoError(err) + orig2, err := NewAccountAttributeMap(testCtx, "test-scope-id", "displayName2", ToFullNameAttribute) + require.NoError(err) + + cp := orig.clone() + assert.True(!proto.Equal(cp.AccountAttributeMap, orig2.AccountAttributeMap)) + }) +} + +func TestAccountAttributeMap_SetTableName(t *testing.T) { + t.Parallel() + defaultTableName := acctAttributeMapTableName + tests := []struct { + name string + setNameTo string + want string + }{ + { + name: "new-name", + setNameTo: "new-name", + want: "new-name", + }, + { + name: "reset to default", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + def := AllocAccountAttributeMap() + require.Equal(defaultTableName, def.TableName()) + m := AllocAccountAttributeMap() + m.SetTableName(tc.setNameTo) + assert.Equal(tc.want, m.TableName()) + }) + } +} + +func TestParseAccountAttributeMaps(t *testing.T) { + t.Parallel() + testCtx := context.Background() + tests := []struct { + name string + ctx context.Context + attrMaps []string + want []AttributeMap + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "dup-to-attribute", + ctx: testCtx, + attrMaps: []string{"from=email", "from=email"}, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "duplicate map for \"email\" attribute", + }, + { + name: "dup-from-attribute", + ctx: testCtx, + attrMaps: []string{"from=email", "from=fullName"}, + want: []AttributeMap{ + {To: "email", From: "from"}, + {To: "fullName", From: "from"}, + }, + }, + { + name: "two-equals", + ctx: testCtx, + attrMaps: []string{"from==email"}, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "\"=email\" is not a valid ToAccountAttribute", + }, + { + name: "invalid-parts", + ctx: testCtx, + attrMaps: []string{"from=e=mail"}, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "\"e=mail\" is not a valid ToAccountAttribute", + }, + { + name: "missing-separators", + ctx: testCtx, + attrMaps: []string{"from/email"}, + + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "error parsing attribute map \"from/email\": format must be key=value", + }, + { + name: "simple", + ctx: testCtx, + attrMaps: []string{"from=email"}, + want: []AttributeMap{ + {To: "email", From: "from"}, + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := ParseAccountAttributeMaps(tc.ctx, tc.attrMaps...) + if tc.wantErrMatch != nil { + require.Error(err) + assert.Empty(got) + assert.True(errors.Match(tc.wantErrMatch, err)) + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + require.NotEmpty(got) + assert.Equal(tc.want, got) + }) + } +} + +func FuzzParseAccountAttributeMaps(f *testing.F) { + reverseFn := func(am AttributeMap) string { + return fmt.Sprintf("%s=%s", am.From, am.To) + } + testCtx := context.Background() + f.Add("mail=email") + f.Add("displayName=fullName") + f.Fuzz(func(t *testing.T, m string) { + am, err := ParseAccountAttributeMaps(testCtx, m) + if err != nil { + return + } + if len(am) != 1 { + return + } + reversed := reverseFn(am[0]) + if reversed != m { + t.Errorf("account attribute roundtrip failed, input %q, output %q", m, reversed) + } + }) +} diff --git a/internal/auth/ldap/account_test.go b/internal/auth/ldap/account_test.go new file mode 100644 index 0000000000..9059a300c3 --- /dev/null +++ b/internal/auth/ldap/account_test.go @@ -0,0 +1,417 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/auth/ldap/store" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestNewAccount(t *testing.T) { + t.Parallel() + testCtx := context.Background() + testAuthMethodId := fmt.Sprintf("%s_1", globals.LdapAuthMethodPrefix) + tests := []struct { + name string + ctx context.Context + scopeId string + authMethodId string + loginName string + opt []Option + want *Account + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "success-with-all-opts", + ctx: testCtx, + scopeId: "global", + authMethodId: testAuthMethodId, + loginName: "test-login-name", + opt: []Option{ + WithName(testCtx, "test-name"), + WithDescription(testCtx, "test-description"), + WithEmail(testCtx, "alice@bob.com"), + WithFullName(testCtx, "alice eve smith"), + WithDn(testCtx, "uid=alice, ou=people, o=test org"), + WithMemberOfGroups(testCtx, "test-group"), + }, + want: &Account{ + Account: &store.Account{ + AuthMethodId: testAuthMethodId, + ScopeId: "global", + LoginName: "test-login-name", + Name: "test-name", + Description: "test-description", + Email: "alice@bob.com", + FullName: "alice eve smith", + Dn: "uid=alice, ou=people, o=test org", + MemberOfGroups: "[\"test-group\"]", + }, + }, + }, + { + name: "missing-scope-id", + ctx: testCtx, + authMethodId: testAuthMethodId, + loginName: "test-login-name", + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing scope id", + }, + { + name: "missing-auth-method-id", + ctx: testCtx, + scopeId: "global", + loginName: "test-login-name", + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing auth method id", + }, + { + name: "missing-login-name", + ctx: testCtx, + scopeId: "global", + authMethodId: testAuthMethodId, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing login name", + }, + { + name: "validate-err", + ctx: testCtx, + scopeId: "global", + authMethodId: testAuthMethodId, + loginName: "test-login-name", + opt: []Option{WithEmail(testCtx, strings.Repeat("-", 400))}, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "email address is too long", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := NewAccount(tc.ctx, tc.scopeId, tc.authMethodId, tc.loginName, tc.opt...) + if tc.wantErrMatch != nil { + require.Error(err) + assert.Nil(got) + assert.True(errors.Match(tc.wantErrMatch, err)) + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.Equal(tc.want, got) + }) + } +} + +func TestAccount_clone(t *testing.T) { + t.Parallel() + testCtx := context.TODO() + testConn, _ := db.TestSetup(t, "postgres") + testWrapper := db.TestWrapper(t) + + t.Run("valid", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper)) + m := TestAuthMethod(t, testConn, testWrapper, org.PublicId, []string{"ldaps://ldap1"}) + orig, err := NewAccount(testCtx, m.ScopeId, m.PublicId, "alice", WithFullName(testCtx, "Alice Eve Smith"), WithEmail(testCtx, "alice@alice.com")) + require.NoError(err) + cp := orig.clone() + assert.True(proto.Equal(cp.Account, orig.Account)) + }) + t.Run("not-equal", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper)) + m := TestAuthMethod(t, testConn, testWrapper, org.PublicId, []string{"ldaps://ldap1"}) + orig, err := NewAccount(testCtx, m.ScopeId, m.PublicId, "alice", WithFullName(testCtx, "Alice Eve Smith"), WithEmail(testCtx, "alice@alice.com")) + require.NoError(err) + orig2, err := NewAccount(testCtx, m.ScopeId, m.PublicId, "bob", WithFullName(testCtx, "Bob Eve Smith"), WithEmail(testCtx, "bob@alice.com")) + require.NoError(err) + + cp := orig.clone() + assert.True(!proto.Equal(cp.Account, orig2.Account)) + }) +} + +func TestAccount_SetTableName(t *testing.T) { + t.Parallel() + defaultTableName := accountTableName + tests := []struct { + name string + setNameTo string + want string + }{ + { + name: "new-name", + setNameTo: "new-name", + want: "new-name", + }, + { + name: "reset to default", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + def := AllocAccount() + require.Equal(defaultTableName, def.TableName()) + m := AllocAccount() + m.SetTableName(tc.setNameTo) + assert.Equal(tc.want, m.TableName()) + }) + } +} + +func TestAccount_oplog(t *testing.T) { + t.Parallel() + testCtx := context.Background() + testAcct, err := NewAccount(testCtx, "global", "test-id", "test-login-name") + testAcct.PublicId = "test-public-id" + require.NoError(t, err) + tests := []struct { + name string + ctx context.Context + acct *Account + opType oplog.OpType + want oplog.Metadata + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "create", + ctx: testCtx, + acct: testAcct, + opType: oplog.OpType_OP_TYPE_CREATE, + want: oplog.Metadata{ + "auth-method-id": {"test-id"}, + "resource-public-id": {"test-public-id"}, + "scope-id": {"global"}, + "op-type": {oplog.OpType_OP_TYPE_CREATE.String()}, + "resource-type": {"ldap account"}, + }, + }, + { + name: "update", + ctx: testCtx, + acct: testAcct, + opType: oplog.OpType_OP_TYPE_UPDATE, + want: oplog.Metadata{ + "auth-method-id": {"test-id"}, + "resource-public-id": {"test-public-id"}, + "scope-id": {"global"}, + "op-type": {oplog.OpType_OP_TYPE_UPDATE.String()}, + "resource-type": {"ldap account"}, + }, + }, + { + name: "missing-auth-method-id", + ctx: testCtx, + acct: func() *Account { + cp := testAcct.clone() + cp.AuthMethodId = "" + return cp + }(), + opType: oplog.OpType_OP_TYPE_UPDATE, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing auth method id", + }, + { + name: "missing-scope-id", + ctx: testCtx, + acct: func() *Account { + cp := testAcct.clone() + cp.ScopeId = "" + return cp + }(), + opType: oplog.OpType_OP_TYPE_UPDATE, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing scope id", + }, + { + name: "missing-public-id", + ctx: testCtx, + acct: func() *Account { + cp := testAcct.clone() + cp.PublicId = "" + return cp + }(), + opType: oplog.OpType_OP_TYPE_UPDATE, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing public id", + }, + { + name: "missing-op-type", + ctx: testCtx, + acct: testAcct, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing op type", + }, + { + name: "missing-account", + ctx: testCtx, + opType: oplog.OpType_OP_TYPE_UPDATE, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing account", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := tc.acct.oplog(tc.ctx, tc.opType) + if tc.wantErrMatch != nil { + require.Error(err) + assert.Nil(got) + assert.True(errors.Match(tc.wantErrMatch, err)) + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.Equal(tc.want, got) + }) + } +} + +func TestAccount_GetAuthMethodId(t *testing.T) { + t.Parallel() + assert.Empty(t, AllocAccount().GetAuthMethodId()) +} + +func TestAccount_GetSubject(t *testing.T) { + t.Parallel() + assert.Empty(t, AllocAccount().GetSubject()) +} + +func TestAccount_validate(t *testing.T) { + t.Parallel() + testCtx := context.Background() + tests := []struct { + name string + ctx context.Context + caller errors.Op + acct *Account + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "missing-caller", + ctx: testCtx, + acct: func() *Account { + a, err := NewAccount(testCtx, "global", "test-auth-method-id", "test-login-name") + require.NoError(t, err) + return a + }(), + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing caller", + }, + { + name: "missing-scope-id", + ctx: testCtx, + caller: "test", + acct: func() *Account { + a, err := NewAccount(testCtx, "global", "test-auth-method-id", "test-login-name") + require.NoError(t, err) + a.ScopeId = "" + return a + }(), + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing scope id", + }, + { + name: "missing-auth-method-id", + ctx: testCtx, + caller: "test", + acct: func() *Account { + a, err := NewAccount(testCtx, "global", "test-auth-method-id", "test-login-name") + require.NoError(t, err) + a.AuthMethodId = "" + return a + }(), + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing auth method id", + }, + { + name: "missing-login-name", + ctx: testCtx, + caller: "test", + acct: func() *Account { + a, err := NewAccount(testCtx, "global", "test-auth-method-id", "test-login-name") + require.NoError(t, err) + a.LoginName = "" + return a + }(), + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing login name", + }, + { + name: "email-too-long", + ctx: testCtx, + caller: "test", + acct: func() *Account { + a, err := NewAccount(testCtx, "global", "test-auth-method-id", "test-login-name") + require.NoError(t, err) + a.Email = strings.Repeat("-", 321) + return a + }(), + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "email address is too long", + }, + { + name: "full-name-too-long", + ctx: testCtx, + caller: "test", + acct: func() *Account { + a, err := NewAccount(testCtx, "global", "test-auth-method-id", "test-login-name") + require.NoError(t, err) + a.FullName = strings.Repeat("-", 513) + return a + }(), + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "full name is too long", + }, + { + name: "login-name-not-lower-case", + ctx: testCtx, + caller: "test", + acct: func() *Account { + a, err := NewAccount(testCtx, "global", "test-auth-method-id", "test-login-name") + require.NoError(t, err) + a.LoginName = "Test-Login-Name" + return a + }(), + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "login name must be lower case", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + err := tc.acct.validate(tc.ctx, tc.caller) + if tc.wantErrMatch != nil { + require.Error(err) + assert.True(errors.Match(tc.wantErrMatch, err)) + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + }) + } +} diff --git a/internal/auth/ldap/auth_method.go b/internal/auth/ldap/auth_method.go new file mode 100644 index 0000000000..95fca24420 --- /dev/null +++ b/internal/auth/ldap/auth_method.go @@ -0,0 +1,319 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "fmt" + "net/url" + + "github.com/hashicorp/boundary/internal/auth/ldap/store" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/oplog" + "google.golang.org/protobuf/proto" +) + +// authMethodTableName defines an AuthMethod's table name. +const authMethodTableName = "auth_ldap_method" + +// AuthMethod contains an LDAP auth method configuration. It is owned by a +// scope. AuthMethods MUST have at least one Url. AuthMethods MAY one or zero: +// UserEntrySearchConf, a GroupEntrySearchConf, BindCredential. AuthMethods +// may have zero to many: Accounts, Certificates, +type AuthMethod struct { + *store.AuthMethod + tableName string +} + +// NewAuthMethod creates a new in memory AuthMethod assigned to a scopeId. The +// new auth method will have an OperationalState of Inactive. +// +// Supports the options: WithUrls, WithName, WithDescription, WithStartTLS, +// WithInsecureTLS, WithDiscoverDN, WithAnonGroupSearch, WithUpnDomain, +// WithUserSearchConf, WithGroupSearchConf, WithCertificates, WithBindCredential +// are the only valid options and all other options are ignored. +func NewAuthMethod(ctx context.Context, scopeId string, opt ...Option) (*AuthMethod, error) { + const op = "ldap.NewAuthMethod" + switch { + case scopeId == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope id") + } + opts, err := getOpts(opt...) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + a := &AuthMethod{ + AuthMethod: &store.AuthMethod{ + ScopeId: scopeId, + Name: opts.withName, + Description: opts.withDescription, + OperationalState: string(opts.withOperationalState), // if no option is specified, a new auth method is initially inactive + Urls: opts.withUrls, + StartTls: opts.withStartTls, + InsecureTls: opts.withInsecureTls, + DiscoverDn: opts.withDiscoverDn, + AnonGroupSearch: opts.withAnonGroupSearch, + UpnDomain: opts.withUpnDomain, + UserDn: opts.withUserDn, + UserAttr: opts.withUserAttr, + UserFilter: opts.withUserFilter, + EnableGroups: opts.withEnableGroups, + UseTokenGroups: opts.withUseTokenGroups, + GroupDn: opts.withGroupDn, + GroupAttr: opts.withGroupAttr, + GroupFilter: opts.withGroupFilter, + BindDn: opts.withBindDn, + BindPassword: opts.withBindPassword, + Certificates: opts.withCertificates, + ClientCertificate: opts.withClientCertificate, + ClientCertificateKey: opts.withClientCertificateKey, + }, + } + if len(opts.withAccountAttributeMap) > 0 { + a.AccountAttributeMaps = make([]string, 0, len(opts.withAccountAttributeMap)) + for k, v := range opts.withAccountAttributeMap { + a.AccountAttributeMaps = append(a.AccountAttributeMaps, fmt.Sprintf("%s=%s", k, v)) + } + } + + return a, nil +} + +// AllocAuthMethod makes an empty one in memory +func AllocAuthMethod() AuthMethod { + return AuthMethod{ + AuthMethod: &store.AuthMethod{}, + } +} + +// clone an AuthMethod +func (am *AuthMethod) clone() *AuthMethod { + cp := proto.Clone(am.AuthMethod) + return &AuthMethod{ + AuthMethod: cp.(*store.AuthMethod), + } +} + +// TableName returns the table name (func is required by gorm) +func (am *AuthMethod) TableName() string { + if am.tableName != "" { + return am.tableName + } + return authMethodTableName +} + +// SetTableName sets the table name (func is required by oplog) +func (am *AuthMethod) SetTableName(n string) { + am.tableName = n +} + +// oplog will create oplog metadata for the AuthMethod. +func (am *AuthMethod) oplog(ctx context.Context, opType oplog.OpType) (oplog.Metadata, error) { + const op = "ldap.(AuthMethod).oplog" + switch { + case am == nil: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method") + case opType == oplog.OpType_OP_TYPE_UNSPECIFIED: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing op type") + case am.PublicId == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing public id") + case am.ScopeId == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope id") + } + metadata := oplog.Metadata{ + "resource-public-id": []string{am.PublicId}, + "resource-type": []string{"ldap auth method"}, + "op-type": []string{opType.String()}, + "scope-id": []string{am.ScopeId}, + } + return metadata, nil +} + +type convertedValues struct { + Urls []any + Certs []any + UserEntrySearchConf any + GroupEntrySearchConf any + ClientCertificate any + BindCredential any + AccountAttributeMaps []any +} + +// convertValueObjects converts the embedded value objects. It will return an +// error if the AuthMethod's public id is not set. +func (am *AuthMethod) convertValueObjects(ctx context.Context) (*convertedValues, error) { + const op = "ldap.(AuthMethod).convertValueObjects" + if am.PublicId == "" { + return nil, errors.New(ctx, errors.InvalidPublicId, op, "missing public id") + } + var err error + converted := &convertedValues{} + + if converted.Urls, err = am.convertUrls(ctx); err != nil { + return nil, errors.Wrap(ctx, err, op) + } + if converted.Certs, err = am.convertCertificates(ctx); err != nil { + return nil, errors.Wrap(ctx, err, op) + } + if am.UserDn != "" || am.UserAttr != "" || am.UserFilter != "" { + if converted.UserEntrySearchConf, err = am.convertUserEntrySearchConf(ctx); err != nil { + return nil, errors.Wrap(ctx, err, op) + } + } + if am.GroupDn != "" || am.GroupAttr != "" || am.GroupFilter != "" { + if converted.GroupEntrySearchConf, err = am.convertGroupEntrySearchConf(ctx); err != nil { + return nil, errors.Wrap(ctx, err, op) + } + } + if am.ClientCertificate != "" || am.ClientCertificateKey != nil { + if converted.ClientCertificate, err = am.convertClientCertificate(ctx); err != nil { + return nil, errors.Wrap(ctx, err, op) + } + } + if am.BindDn != "" || am.BindPassword != "" { + if converted.BindCredential, err = am.convertBindCredential(ctx); err != nil { + return nil, errors.Wrap(ctx, err, op) + } + } + if converted.AccountAttributeMaps, err = am.convertAccountAttributeMaps(ctx); err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + return converted, nil +} + +// convertCertificates converts any embedded URLs from []string +// to []any where each slice element is a *Url. It will return an error if the +// AuthMethod's public id is not set. +func (am *AuthMethod) convertUrls(ctx context.Context) ([]any, error) { + const op = "ldap.(AuthMethod).convertUrls" + if am.PublicId == "" { + return nil, errors.New(ctx, errors.InvalidPublicId, op, "missing public id") + } + newValObjs := make([]any, 0, len(am.Urls)) + for priority, u := range am.Urls { + parsed, err := url.Parse(u) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + obj, err := NewUrl(ctx, am.PublicId, priority+1, parsed) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + newValObjs = append(newValObjs, obj) + } + return newValObjs, nil +} + +// convertCertificates converts any embedded certificates from []string +// to []any where each slice element is a *Certificate. It will return an error +// if the AuthMethod's public id is not set. +func (am *AuthMethod) convertCertificates(ctx context.Context) ([]any, error) { + const op = "ldap.(AuthMethod).convertCertificates" + if am.PublicId == "" { + return nil, errors.New(ctx, errors.InvalidPublicId, op, "missing public id") + } + newValObjs := make([]any, 0, len(am.Certificates)) + for _, cert := range am.Certificates { + obj, err := NewCertificate(ctx, am.PublicId, cert) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + newValObjs = append(newValObjs, obj) + } + return newValObjs, nil +} + +// convertUserEntrySearchConf converts an embedded user entry search fields +// into an any type. It will return an error if the AuthMethod's public id is +// not set. +func (am *AuthMethod) convertUserEntrySearchConf(ctx context.Context) (any, error) { + const op = "ldap.(AuthMethod).convertUserEntrySearchConf" + if am.PublicId == "" { + return nil, errors.New(ctx, errors.InvalidPublicId, op, "missing public id") + } + c, err := NewUserEntrySearchConf(ctx, am.PublicId, WithUserDn(ctx, am.UserDn), WithUserAttr(ctx, am.UserAttr), WithUserFilter(ctx, am.UserFilter)) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + return c, nil +} + +// convertGroupEntrySearchConf converts an embedded group entry search fields +// into an any type. It will return an error if the AuthMethod's public id is +// not set. +func (am *AuthMethod) convertGroupEntrySearchConf(ctx context.Context) (any, error) { + const op = "ldap.(AuthMethod).convertGroupEntrySearchConf" + if am.PublicId == "" { + return nil, errors.New(ctx, errors.InvalidPublicId, op, "missing public id") + } + c, err := NewGroupEntrySearchConf(ctx, am.PublicId, WithGroupDn(ctx, am.GroupDn), WithGroupAttr(ctx, am.GroupAttr), WithGroupFilter(ctx, am.GroupFilter)) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + return c, nil +} + +// convertClientCertificate converts an embedded client certificate entry into +// an any type. It will return an error if the AuthMethod's public id is not +// set. +func (am *AuthMethod) convertClientCertificate(ctx context.Context) (any, error) { + const op = "ldap.(AuthMethod).convertClientCertificate" + if am.PublicId == "" { + return nil, errors.New(ctx, errors.InvalidPublicId, op, "missing auth method id") + } + cc, err := NewClientCertificate(ctx, am.PublicId, am.ClientCertificateKey, am.ClientCertificate) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + return cc, nil +} + +// convertBindCredential converts an embedded bind credential entry into +// an any type. It will return an error if the AuthMethod's public id is not +// set. +func (am *AuthMethod) convertBindCredential(ctx context.Context) (any, error) { + const op = "ldap.(AuthMethod).convertBindCredentials" + if am.PublicId == "" { + return nil, errors.New(ctx, errors.InvalidPublicId, op, "missing auth method id") + } + bc, err := NewBindCredential(ctx, am.PublicId, am.BindDn, []byte(am.BindPassword)) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + return bc, nil +} + +// convertAccountAttributeMaps converts the embedded account attribute maps from +// []string to []interface{} where each slice element is a *AccountAttributeMap. It +// will return an error if the AuthMethod's public id is not set or it can +// convert the account attribute maps. +func (am *AuthMethod) convertAccountAttributeMaps(ctx context.Context) ([]any, error) { + const op = "ldap.(AuthMethod).convertAccountAttributeMaps" + if am.PublicId == "" { + return nil, errors.New(ctx, errors.InvalidPublicId, op, "missing public id") + } + newInterfaces := make([]any, 0, len(am.AccountAttributeMaps)) + const ( + from = 0 + to = 1 + ) + acms, err := ParseAccountAttributeMaps(ctx, am.AccountAttributeMaps...) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + for _, m := range acms { + toClaim, err := ConvertToAccountToAttribute(ctx, m.To) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + obj, err := NewAccountAttributeMap(ctx, am.PublicId, m.From, toClaim) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + newInterfaces = append(newInterfaces, obj) + } + return newInterfaces, nil +} diff --git a/internal/auth/ldap/auth_method_test.go b/internal/auth/ldap/auth_method_test.go new file mode 100644 index 0000000000..128e3dc78f --- /dev/null +++ b/internal/auth/ldap/auth_method_test.go @@ -0,0 +1,594 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "crypto/ed25519" + "crypto/rand" + "crypto/x509" + "net/url" + "sort" + "testing" + + "github.com/hashicorp/boundary/internal/auth/ldap/store" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestNewAuthMethod(t *testing.T) { + t.Parallel() + testCtx := context.Background() + testCert, testCertEncoded := TestGenerateCA(t, "localhost") + _, testPrivKey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + derPrivKey, err := x509.MarshalPKCS8PrivateKey(testPrivKey) + require.NoError(t, err) + + tests := []struct { + name string + ctx context.Context + scopeId string + urls []*url.URL + opts []Option + want *AuthMethod + wantErr bool + wantErrCode errors.Code + wantErrContains string + }{ + { + name: "valid-all-opts", + ctx: testCtx, + scopeId: "global", + urls: TestConvertToUrls(t, "ldaps://alice.com"), // converted to an option + opts: []Option{ + WithName(testCtx, "test-name"), + WithDescription(testCtx, "test-description"), + WithStartTLS(testCtx), + WithInsecureTLS(testCtx), + WithDiscoverDn(testCtx), + WithAnonGroupSearch(testCtx), + WithUpnDomain(testCtx, "alice.com"), + WithUserDn(testCtx, "user-dn"), + WithUserAttr(testCtx, "user-attr"), + WithUserFilter(testCtx, "user-filter"), + WithGroupDn(testCtx, "group-dn"), + WithEnableGroups(testCtx), + WithGroupAttr(testCtx, "group-attr"), + WithGroupFilter(testCtx, "group-filter"), + WithBindCredential(testCtx, "bind-dn", "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": "email"}), + }, + want: &AuthMethod{ + AuthMethod: &store.AuthMethod{ + ScopeId: "global", + Urls: []string{"ldaps://alice.com"}, + OperationalState: string(InactiveState), + Name: "test-name", + Description: "test-description", + StartTls: true, + InsecureTls: true, + DiscoverDn: true, + AnonGroupSearch: true, + UpnDomain: "alice.com", + UserDn: "user-dn", + UserAttr: "user-attr", + UserFilter: "user-filter", + EnableGroups: true, + GroupDn: "group-dn", + GroupAttr: "group-attr", + GroupFilter: "group-filter", + BindDn: "bind-dn", + BindPassword: "bind-password", + Certificates: []string{testCertEncoded}, + ClientCertificate: testCertEncoded, + ClientCertificateKey: derPrivKey, + AccountAttributeMaps: []string{"mail=email"}, + }, + }, + }, + { + name: "valid-no-opts", + ctx: testCtx, + scopeId: "global", + urls: TestConvertToUrls(t, "ldaps://alice.com"), + want: &AuthMethod{ + AuthMethod: &store.AuthMethod{ + ScopeId: "global", + Urls: []string{"ldaps://alice.com"}, + OperationalState: string(InactiveState), + }, + }, + }, + { + name: "missing-scope", + ctx: testCtx, + urls: TestConvertToUrls(t, "ldaps://alice.com"), + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "missing scope id", + }, + { + name: "invalid-urls", + ctx: testCtx, + scopeId: "global", + urls: func() []*url.URL { + parsed, err := url.Parse("https://alice.com") + require.NoError(t, err) + return []*url.URL{parsed} + }(), + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: `https scheme in url "https://alice.com" is not either ldap or ldaps`, + }, + { + name: "opt-error", + ctx: testCtx, + scopeId: "global", + urls: TestConvertToUrls(t, "ldaps://alice.com"), + opts: []Option{WithBindCredential(testCtx, "dn", "")}, + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "ldap.WithBindCredential: missing password", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + tc.opts = append(tc.opts, WithUrls(tc.ctx, tc.urls...)) + am, err := NewAuthMethod(tc.ctx, tc.scopeId, tc.opts...) + if tc.wantErr { + require.Error(err) + if tc.wantErrCode != errors.Unknown { + assert.True(errors.Match(errors.T(tc.wantErrCode), err)) + } + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.Equal(tc.want, am) + }) + } +} + +func TestAuthMethod_SetTableName(t *testing.T) { + t.Parallel() + defaultTableName := authMethodTableName + tests := []struct { + name string + setNameTo string + want string + }{ + { + name: "new-name", + setNameTo: "new-name", + want: "new-name", + }, + { + name: "reset to default", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + def := AllocAuthMethod() + require.Equal(defaultTableName, def.TableName()) + m := AllocAuthMethod() + m.SetTableName(tt.setNameTo) + assert.Equal(tt.want, m.TableName()) + }) + } +} + +func TestAuthMethod_clone(t *testing.T) { + t.Parallel() + testCtx := context.Background() + t.Run("valid", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + testCert, _ := TestGenerateCA(t, "localhost") + _, testPrivKey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(err) + derPrivKey, err := x509.MarshalPKCS8PrivateKey(testPrivKey) + require.NoError(err) + am, err := NewAuthMethod( + testCtx, + "global", + WithUrls(testCtx, TestConvertToUrls(t, "ldaps://alice.com")...), + WithStartTLS(testCtx), + WithInsecureTLS(testCtx), + WithDiscoverDn(testCtx), + WithAnonGroupSearch(testCtx), + WithUpnDomain(testCtx, "alice.com"), + WithUserDn(testCtx, "user-dn"), + WithUserAttr(testCtx, "user-attr"), + WithUserFilter(testCtx, "user-filter"), + WithGroupDn(testCtx, "group-dn"), + WithGroupAttr(testCtx, "group-attr"), + WithGroupFilter(testCtx, "group-filter"), + WithBindCredential(testCtx, "bind-dn", "bind-password"), + WithCertificates(testCtx, testCert), + WithClientCertificate(testCtx, derPrivKey, testCert), // not a client cert but good enough for this test. + ) + require.NoError(err) + cp := am.clone() + assert.True(proto.Equal(cp.AuthMethod, am.AuthMethod)) + }) + t.Run("not-equal", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + am, err := NewAuthMethod(testCtx, "global", WithUrls(testCtx, TestConvertToUrls(t, "ldaps://alice.com")...)) + require.NoError(err) + am2, err := NewAuthMethod(testCtx, "global", WithUrls(testCtx, TestConvertToUrls(t, "ldaps://bob.com")...)) + require.NoError(err) + + cp := am.clone() + assert.True(!proto.Equal(cp.AuthMethod, am2.AuthMethod)) + }) +} + +func TestAuthMethod_oplog(t *testing.T) { + t.Parallel() + testCtx := context.Background() + testAm, err := NewAuthMethod(testCtx, "global", WithUrls(testCtx, TestConvertToUrls(t, "ldap://ldap1")...)) + testAm.PublicId = "test-public-id" + require.NoError(t, err) + tests := []struct { + name string + ctx context.Context + am *AuthMethod + opType oplog.OpType + want oplog.Metadata + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "create", + ctx: testCtx, + am: testAm, + opType: oplog.OpType_OP_TYPE_CREATE, + want: oplog.Metadata{ + "resource-public-id": {"test-public-id"}, + "scope-id": {"global"}, + "op-type": {oplog.OpType_OP_TYPE_CREATE.String()}, + "resource-type": {"ldap auth method"}, + }, + }, + { + name: "update", + ctx: testCtx, + am: testAm, + opType: oplog.OpType_OP_TYPE_UPDATE, + want: oplog.Metadata{ + "resource-public-id": {"test-public-id"}, + "scope-id": {"global"}, + "op-type": {oplog.OpType_OP_TYPE_UPDATE.String()}, + "resource-type": {"ldap auth method"}, + }, + }, + { + name: "missing-scope-id", + ctx: testCtx, + am: func() *AuthMethod { + cp := testAm.clone() + cp.ScopeId = "" + return cp + }(), + opType: oplog.OpType_OP_TYPE_UPDATE, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing scope id", + }, + { + name: "missing-public-id", + ctx: testCtx, + am: func() *AuthMethod { + cp := testAm.clone() + cp.PublicId = "" + return cp + }(), + opType: oplog.OpType_OP_TYPE_UPDATE, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing public id", + }, + { + name: "missing-op-type", + ctx: testCtx, + am: testAm, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing op type", + }, + { + name: "missing-auth-method", + ctx: testCtx, + opType: oplog.OpType_OP_TYPE_UPDATE, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing auth method", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := tc.am.oplog(tc.ctx, tc.opType) + if tc.wantErrMatch != nil { + require.Error(err) + assert.Nil(got) + assert.True(errors.Match(tc.wantErrMatch, err)) + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.Equal(tc.want, got) + }) + } +} + +func Test_convertValueObjects(t *testing.T) { + testCtx := context.TODO() + testPublicId := "test-id" + testLdapServers := []string{"ldaps://ldap1.alice.com", "ldaps://ldap2.alice.com"} + _, pem := TestGenerateCA(t, "localhost") + testCerts := []string{pem} + c, err := NewCertificate(testCtx, testPublicId, pem) + require.NoError(t, err) + testCertificates := []any{c} + + testUrls := make([]any, 0, len(testLdapServers)) + for priority, uu := range TestConvertToUrls(t, testLdapServers...) { + u, err := NewUrl(testCtx, testPublicId, priority+1, uu) + require.NoError(t, err) + testUrls = append(testUrls, u) + } + + testAttrMaps := []string{"email_address=email", "display_name=fullName"} + testAccountAttributeMaps := make([]any, 0, len(testAttrMaps)) + acms, err := ParseAccountAttributeMaps(testCtx, testAttrMaps...) + require.NoError(t, err) + for _, m := range acms { + toAttribute, err := ConvertToAccountToAttribute(testCtx, m.To) + require.NoError(t, err) + obj, err := NewAccountAttributeMap(testCtx, testPublicId, m.From, toAttribute) + require.NoError(t, err) + testAccountAttributeMaps = append(testAccountAttributeMaps, obj) + } + + testUserSearchConf, err := NewUserEntrySearchConf(testCtx, testPublicId, WithUserDn(testCtx, "user-dn"), WithUserAttr(testCtx, "user-attr")) + require.NoError(t, err) + + testGroupSearchConf, err := NewGroupEntrySearchConf(testCtx, testPublicId, WithGroupDn(testCtx, "group-dn"), WithGroupAttr(testCtx, "group-attr")) + require.NoError(t, err) + + _, testClientCertEncoded := TestGenerateCA(t, "client-cert-host") + _, testPrivKey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + + testClientCertKey, err := x509.MarshalPKCS8PrivateKey(testPrivKey) + require.NoError(t, err) + + testClientCertificate, err := NewClientCertificate(testCtx, testPublicId, testClientCertKey, testClientCertEncoded) + require.NoError(t, err) + + testBindCredential, err := NewBindCredential(testCtx, testPublicId, "bind-dn", []byte("bind-password")) + require.NoError(t, err) + + tests := []struct { + name string + am *AuthMethod + wantValues *convertedValues + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "success", + am: &AuthMethod{ + AuthMethod: &store.AuthMethod{ + PublicId: testPublicId, + Certificates: testCerts, + Urls: testLdapServers, + UserDn: "user-dn", + UserAttr: "user-attr", + GroupDn: "group-dn", + GroupAttr: "group-attr", + ClientCertificateKey: testClientCertKey, + ClientCertificate: testClientCertEncoded, + BindDn: "bind-dn", + BindPassword: "bind-password", + AccountAttributeMaps: testAttrMaps, + }, + }, + wantValues: &convertedValues{ + Certs: testCertificates, + Urls: testUrls, + UserEntrySearchConf: testUserSearchConf, + GroupEntrySearchConf: testGroupSearchConf, + ClientCertificate: testClientCertificate, + BindCredential: testBindCredential, + AccountAttributeMaps: testAccountAttributeMaps, + }, + }, + { + name: "missing-public-id", + am: &AuthMethod{ + AuthMethod: &store.AuthMethod{ + Certificates: testCerts, + Urls: testLdapServers, + UserDn: "user-dn", + UserAttr: "user-attr", + GroupDn: "group-dn", + GroupAttr: "group-attr", + ClientCertificateKey: testClientCertKey, + ClientCertificate: testClientCertEncoded, + BindDn: "bind-dn", + BindPassword: "bind-password", + }, + }, + wantErrMatch: errors.T(errors.InvalidPublicId), + }, + { + name: "invalid-to-account-attr-map", + am: &AuthMethod{ + AuthMethod: &store.AuthMethod{ + PublicId: testPublicId, + AccountAttributeMaps: []string{"displayName=invalid-to-attr"}, + }, + }, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "not a valid ToAccountAttribute value", + }, + { + name: "invalid-account-attr-map-format", + am: &AuthMethod{ + AuthMethod: &store.AuthMethod{ + PublicId: testPublicId, + AccountAttributeMaps: []string{"not-valid"}, + }, + }, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "format must be key=value", + }, + { + name: "invalid-cert", + am: &AuthMethod{ + AuthMethod: &store.AuthMethod{ + PublicId: testPublicId, + Certificates: []string{TestInvalidPem}, + }, + }, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "failed to parse certificate", + }, + { + name: "invalid-url-scheme", + am: &AuthMethod{ + AuthMethod: &store.AuthMethod{ + PublicId: testPublicId, + Urls: []string{"https://ldap1"}, + }, + }, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "scheme \"https\" is not ldap or ldaps", + }, + { + name: "invalid-url-starts-with-space", + am: &AuthMethod{ + AuthMethod: &store.AuthMethod{ + PublicId: testPublicId, + Urls: []string{" ldaps://ldap1"}, + }, + }, + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "first path segment in URL cannot contain colon", + }, + { + name: "invalid-client-cert", + am: &AuthMethod{ + AuthMethod: &store.AuthMethod{ + PublicId: testPublicId, + ClientCertificateKey: testClientCertKey, + ClientCertificate: TestInvalidPem, + }, + }, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "failed to parse certificate", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + values, err := tc.am.convertValueObjects(testCtx) + if tc.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tc.wantErrMatch, err), "wanted err %q and got: %+v", tc.wantErrMatch.Code, err) + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + testSortConverted(t, tc.wantValues) + testSortConverted(t, values) + assert.Equal(tc.wantValues, values) + }) + } + t.Run("missing-public-id", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + wantErrMatch := errors.T(errors.InvalidPublicId) + am := &AuthMethod{ + AuthMethod: &store.AuthMethod{ + Certificates: testCerts, + Urls: testLdapServers, + UserDn: "user-dn", + UserAttr: "user-attr", + GroupDn: "group-dn", + GroupAttr: "group-attr", + ClientCertificateKey: testClientCertKey, + ClientCertificate: testClientCertEncoded, + BindDn: "bind-dn", + BindPassword: "bind-password", + AccountAttributeMaps: testAttrMaps, + }, + } + convertedCerts, err := am.convertCertificates(testCtx) + require.Error(err) + assert.Nil(convertedCerts) + assert.Truef(errors.Match(wantErrMatch, err), "wanted err %q and got: %+v", wantErrMatch.Code, err) + + convertedUrls, err := am.convertUrls(testCtx) + require.Error(err) + assert.Nil(convertedUrls) + assert.Truef(errors.Match(wantErrMatch, err), "wanted err %q and got: %+v", wantErrMatch.Code, err) + + convertedMaps, err := am.convertAccountAttributeMaps(testCtx) + require.Error(err) + assert.Nil(convertedMaps) + assert.Truef(errors.Match(wantErrMatch, err), "wanted err %q and got: %+v", wantErrMatch.Code, err) + + convertedUserSearchConf, err := am.convertUserEntrySearchConf(testCtx) + require.Error(err) + assert.Nil(convertedUserSearchConf) + assert.Truef(errors.Match(wantErrMatch, err), "wanted err %q and got: %+v", wantErrMatch.Code, err) + + convertedGroupSearchConf, err := am.convertGroupEntrySearchConf(testCtx) + require.Error(err) + assert.Nil(convertedGroupSearchConf) + assert.Truef(errors.Match(wantErrMatch, err), "wanted err %q and got: %+v", wantErrMatch.Code, err) + + convertedClientCertificate, err := am.convertClientCertificate(testCtx) + require.Error(err) + assert.Nil(convertedClientCertificate) + assert.Truef(errors.Match(wantErrMatch, err), "wanted err %q and got: %+v", wantErrMatch.Code, err) + + convertedBindCredential, err := am.convertBindCredential(testCtx) + require.Error(err) + assert.Nil(convertedBindCredential) + assert.Truef(errors.Match(wantErrMatch, err), "wanted err %q and got: %+v", wantErrMatch.Code, err) + }) +} + +type converted []any + +func (a converted) Len() int { return len(a) } +func (a converted) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a converted) Less(i, j int) bool { + switch a[i].(type) { + case *Url: + return a[i].(*Url).GetServerUrl() < a[j].(*Url).GetServerUrl() + case *Certificate: + return a[i].(*Certificate).GetCert() < a[j].(*Certificate).GetCert() + } + return false +} + +func testSortConverted(t *testing.T, c *convertedValues) { + sort.Sort(converted(c.Urls)) + sort.Sort(converted(c.Certs)) +} diff --git a/internal/auth/ldap/bind_credential.go b/internal/auth/ldap/bind_credential.go new file mode 100644 index 0000000000..0787e567ca --- /dev/null +++ b/internal/auth/ldap/bind_credential.go @@ -0,0 +1,104 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + + "github.com/hashicorp/boundary/internal/auth/ldap/store" + "github.com/hashicorp/boundary/internal/errors" + wrapping "github.com/hashicorp/go-kms-wrapping/v2" + "github.com/hashicorp/go-kms-wrapping/v2/extras/structwrapping" + "google.golang.org/protobuf/proto" +) + +const bindCredentialTableName = "auth_ldap_bind_credential" + +// BindCredential represent optional parameters which allow Boundary to bind +// (aka authenticate) using the credentials provided when searching for the user +// entry used to authenticate the end user. +type BindCredential struct { + *store.BindCredential + tableName string +} + +// NewBindCredential creates a new in memory BindCredential. No options are currently supported. +func NewBindCredential(ctx context.Context, authMethodId string, dn string, password []byte, _ ...Option) (*BindCredential, error) { + const op = "ldap.NewBindCredential" + switch { + case authMethodId == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id") + case dn == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing dn") + case len(password) == 0: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing password") + } + return &BindCredential{ + BindCredential: &store.BindCredential{ + LdapMethodId: authMethodId, + Dn: dn, + Password: password, + }, + }, nil +} + +// allocBindCredential makes an empty one in memory +func allocBindCredential() *BindCredential { + return &BindCredential{ + BindCredential: &store.BindCredential{}, + } +} + +// clone a bind credential +func (bc *BindCredential) clone() *BindCredential { + cp := proto.Clone(bc.BindCredential) + return &BindCredential{ + BindCredential: cp.(*store.BindCredential), + } +} + +// TableName returns the table name +func (bc *BindCredential) TableName() string { + if bc.tableName != "" { + return bc.tableName + } + return bindCredentialTableName +} + +// SetTableName sets the table name. +func (bc *BindCredential) SetTableName(n string) { + bc.tableName = n +} + +// encrypt the bind credential before writing it to the database +func (bc *BindCredential) encrypt(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "ldap.(BindCredential).encrypt" + if cipher == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing cipher") + } + if err := structwrapping.WrapStruct(ctx, cipher, bc.BindCredential); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt)) + } + var err error + if bc.KeyId, err = cipher.KeyId(ctx); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("failed to read cipher key id")) + } + if bc.PasswordHmac, err = hmacField(ctx, cipher, bc.Password, bc.LdapMethodId); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("failed to hmac password")) + } + + return nil +} + +// decrypt the bind credential after reading it from the database +func (bc *BindCredential) decrypt(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "ldap.(BindCredential).decrypt" + if cipher == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing cipher") + } + if err := structwrapping.UnwrapStruct(ctx, cipher, bc.BindCredential); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt)) + } + return nil +} diff --git a/internal/auth/ldap/bind_credential_test.go b/internal/auth/ldap/bind_credential_test.go new file mode 100644 index 0000000000..9cea7a07ba --- /dev/null +++ b/internal/auth/ldap/bind_credential_test.go @@ -0,0 +1,283 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/boundary/internal/auth/ldap/store" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + wrapping "github.com/hashicorp/go-kms-wrapping/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestNewBindCredential(t *testing.T) { + t.Parallel() + testCtx := context.Background() + tests := []struct { + name string + ctx context.Context + authMethodId string + dn string + password []byte + want *BindCredential + wantErr bool + wantErrCode errors.Code + wantErrContains string + }{ + { + name: "valid", + ctx: testCtx, + authMethodId: "test-id", + dn: "dn", + password: []byte("password"), + want: &BindCredential{ + BindCredential: &store.BindCredential{ + LdapMethodId: "test-id", + Dn: "dn", + Password: []byte("password"), + }, + }, + }, + { + name: "missing-auth-method-id", + ctx: testCtx, + dn: "dn", + password: []byte("password"), + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "missing auth method id", + }, + { + name: "missing-dn", + ctx: testCtx, + authMethodId: "test-id", + password: []byte("password"), + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "missing dn", + }, + { + name: "missing-password", + ctx: testCtx, + authMethodId: "test-id", + dn: "dn", + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "missing password", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := NewBindCredential(tc.ctx, tc.authMethodId, tc.dn, tc.password) + if tc.wantErr { + require.Error(err) + assert.Nil(got) + if tc.wantErrCode != errors.Unknown { + assert.True(errors.Match(errors.T(tc.wantErrCode), err)) + } + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.Equal(tc.want, got) + }) + } +} + +func TestBindCredential_SetTableName(t *testing.T) { + t.Parallel() + defaultTableName := bindCredentialTableName + tests := []struct { + name string + setNameTo string + want string + }{ + { + name: "new-name", + setNameTo: "new-name", + want: "new-name", + }, + { + name: "reset to default", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + def := allocBindCredential() + require.Equal(defaultTableName, def.TableName()) + m := allocBindCredential() + m.SetTableName(tt.setNameTo) + assert.Equal(tt.want, m.TableName()) + }) + } +} + +func TestBindCredential_clone(t *testing.T) { + t.Parallel() + testCtx := context.Background() + t.Run("valid", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + bc, err := NewBindCredential(testCtx, "test-id", "dn", []byte("password")) + require.NoError(err) + cp := bc.clone() + assert.True(proto.Equal(cp.BindCredential, bc.BindCredential)) + }) + t.Run("not-equal", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + bc, err := NewBindCredential(testCtx, "test-id", "dn2", []byte("password")) + require.NoError(err) + + bc2, err := NewBindCredential(testCtx, "test-id", "dn", []byte("password2")) + require.NoError(err) + + cp := bc.clone() + assert.True(!proto.Equal(cp.BindCredential, bc2.BindCredential)) + }) +} + +func TestBindCredential_encrypt_decrypt(t *testing.T) { + t.Parallel() + testCtx := context.Background() + testWrapper := db.TestWrapper(t) + tests := []struct { + name string + ctx context.Context + cipher wrapping.Wrapper + bc *BindCredential + wantErr bool + wantErrCode errors.Code + wantErrContains string + }{ + { + name: "valid", + ctx: testCtx, + cipher: testWrapper, + bc: func() *BindCredential { + testBindCred, err := NewBindCredential(testCtx, "test-id", "dn", []byte("password")) + require.NoError(t, err) + return testBindCred + }(), + }, + { + name: "missing-cipher", + ctx: testCtx, + bc: func() *BindCredential { + testBindCred, err := NewBindCredential(testCtx, "test-id", "dn", []byte("password")) + require.NoError(t, err) + return testBindCred + }(), + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "missing cipher", + }, + { + name: "encrypt-err", + ctx: testCtx, + cipher: &kms.MockWrapper{ + Wrapper: testWrapper, + EncryptErr: fmt.Errorf("test encrypt error"), + }, + bc: func() *BindCredential { + testBindCred, err := NewBindCredential(testCtx, "test-id", "dn", []byte("password")) + require.NoError(t, err) + return testBindCred + }(), + wantErr: true, + wantErrCode: errors.Encrypt, + wantErrContains: "test encrypt error", + }, + { + name: "keyId-err", + ctx: testCtx, + cipher: &kms.MockWrapper{ + Wrapper: testWrapper, + KeyIdErr: fmt.Errorf("test key id error"), + }, + bc: func() *BindCredential { + testBindCred, err := NewBindCredential(testCtx, "test-id", "dn", []byte("password")) + require.NoError(t, err) + return testBindCred + }(), + wantErr: true, + wantErrCode: errors.Encrypt, + wantErrContains: "test key id error", + }, + { + name: "unknown-wrapper-type-err", + ctx: testCtx, + cipher: &kms.MockWrapper{ + Wrapper: testWrapper, + }, + bc: func() *BindCredential { + testBindCred, err := NewBindCredential(testCtx, "test-id", "dn", []byte("password")) + require.NoError(t, err) + return testBindCred + }(), + wantErr: true, + wantErrCode: errors.Encrypt, + wantErrContains: "failed to hmac password", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + require.Empty(tc.bc.CtPassword) + require.Empty(tc.bc.PasswordHmac) + + err := tc.bc.encrypt(tc.ctx, tc.cipher) + if tc.wantErr { + require.Error(err) + if tc.wantErrCode != errors.Unknown { + assert.True(errors.Match(errors.T(tc.wantErrCode), err)) + } + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.NotEmpty(tc.bc.GetPasswordHmac()) + assert.NotEmpty(tc.bc.GetCtPassword()) + + origPass := make([]byte, len(tc.bc.Password)) + copy(origPass, tc.bc.Password) + tc.bc.Password = nil + require.NoError(tc.bc.decrypt(tc.ctx, tc.cipher)) + assert.NotEmpty(tc.bc.Password) + assert.Equal(origPass, tc.bc.Password) + }) + } + t.Run("decrypt-missing-cipher", func(t *testing.T) { + testBindCred, err := NewBindCredential(testCtx, "test-id", "dn", []byte("password")) + require.NoError(t, err) + err = testBindCred.decrypt(testCtx, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "missing cipher") + }) + t.Run("decrypt-err", func(t *testing.T) { + w := &kms.MockWrapper{ + Wrapper: testWrapper, + DecryptErr: fmt.Errorf("test decrypt error"), + } + testBindCred, err := NewBindCredential(testCtx, "test-id", "dn", []byte("password")) + require.NoError(t, err) + require.NoError(t, testBindCred.encrypt(testCtx, testWrapper)) + err = testBindCred.decrypt(testCtx, w) + require.Error(t, err) + assert.Contains(t, err.Error(), "test decrypt error") + }) +} diff --git a/internal/auth/ldap/certificate.go b/internal/auth/ldap/certificate.go new file mode 100644 index 0000000000..5b710006a2 --- /dev/null +++ b/internal/auth/ldap/certificate.go @@ -0,0 +1,92 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "crypto/x509" + "encoding/pem" + "fmt" + + "github.com/hashicorp/boundary/internal/auth/ldap/store" + "github.com/hashicorp/boundary/internal/errors" + "google.golang.org/protobuf/proto" +) + +const certificateTableName = "auth_ldap_certificate" + +// Certificate defines a certificate to use as part of a trust root when +// connecting to an auth method's LDAP server. It is assigned to an LDAP +// AuthMethod and updates/deletes to that AuthMethod are cascaded to its +// Certificates. Certificates are value objects of an AuthMethod, therefore +// there's no need for oplog metadata, since only the AuthMethod will have +// metadata because it's the root aggregate. +type Certificate struct { + *store.Certificate + tableName string +} + +// NewCertificate creates a new in memory certificate assigned to and LDAP auth +// method. +func NewCertificate(ctx context.Context, authMethodId string, certificatePem string) (*Certificate, error) { + const op = "ldap.NewCertificate" + // validate() will check the parameters. + c := &Certificate{ + Certificate: &store.Certificate{ + LdapMethodId: authMethodId, + Cert: certificatePem, + }, + } + if err := c.validate(ctx, op); err != nil { + return nil, err // intentionally, not wrapping err + } + return c, nil +} + +// validate the Certificate and on success return nil +func (c *Certificate) validate(ctx context.Context, caller errors.Op) error { + switch { + case c.LdapMethodId == "": + return errors.New(ctx, errors.InvalidParameter, caller, "missing ldap auth method id") + case c.Cert == "": + return errors.New(ctx, errors.InvalidParameter, caller, "missing certificate") + default: + blk, _ := pem.Decode([]byte(c.Cert)) + if blk == nil { + return errors.New(ctx, errors.InvalidParameter, caller, "failed to parse certificate: invalid PEM encoding") + } + if _, err := x509.ParseCertificate(blk.Bytes); err != nil { + return errors.New(ctx, errors.InvalidParameter, caller, fmt.Sprintf("failed to parse certificate: invalid block: %s", err.Error()), errors.WithWrap(err)) + } + return nil + } +} + +// allocCertificate makes an empty one in memory +func allocCertificate() Certificate { + return Certificate{ + Certificate: &store.Certificate{}, + } +} + +// clone a Certificate +func (c *Certificate) clone() *Certificate { + cp := proto.Clone(c.Certificate) + return &Certificate{ + Certificate: cp.(*store.Certificate), + } +} + +// TableName returns the table name. +func (c *Certificate) TableName() string { + if c.tableName != "" { + return c.tableName + } + return certificateTableName +} + +// SetTableName sets the table name. +func (c *Certificate) SetTableName(n string) { + c.tableName = n +} diff --git a/internal/auth/ldap/certificate_test.go b/internal/auth/ldap/certificate_test.go new file mode 100644 index 0000000000..5569560049 --- /dev/null +++ b/internal/auth/ldap/certificate_test.go @@ -0,0 +1,152 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/auth/ldap/store" + "github.com/hashicorp/boundary/internal/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestNewCertificate(t *testing.T) { + t.Parallel() + testCtx := context.Background() + _, testCertEncoded := TestGenerateCA(t, "localhost") + tests := []struct { + name string + ctx context.Context + authMethodId string + certPem string + want *Certificate + wantErr bool + wantErrCode errors.Code + wantErrContains string + }{ + { + name: "valid", + ctx: testCtx, + authMethodId: "test-id", + certPem: testCertEncoded, + want: &Certificate{ + Certificate: &store.Certificate{ + LdapMethodId: "test-id", + Cert: testCertEncoded, + }, + }, + }, + { + name: "missing-auth-method-id", + ctx: testCtx, + certPem: testCertEncoded, + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "missing ldap auth method id", + }, + { + name: "missing-cert", + ctx: testCtx, + authMethodId: "test-id", + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "missing certificate", + }, + { + name: "invalid-cert", + ctx: testCtx, + authMethodId: "test-id", + certPem: "not-a-cert", + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "failed to parse certificate: invalid PEM encoding", + }, + { + name: "invalid-block", + ctx: testCtx, + authMethodId: "test-id", + certPem: TestInvalidPem, + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "failed to parse certificate: invalid block", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + gotCert, err := NewCertificate(tc.ctx, tc.authMethodId, tc.certPem) + if tc.wantErr { + require.Error(err) + assert.Nil(gotCert) + if tc.wantErrCode != errors.Unknown { + assert.True(errors.Match(errors.T(tc.wantErrCode), err)) + } + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + }) + } +} + +func TestCertificate_SetTableName(t *testing.T) { + t.Parallel() + defaultTableName := certificateTableName + tests := []struct { + name string + setNameTo string + want string + }{ + { + name: "new-name", + setNameTo: "new-name", + want: "new-name", + }, + { + name: "reset to default", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + def := allocCertificate() + require.Equal(defaultTableName, def.TableName()) + m := allocCertificate() + m.SetTableName(tt.setNameTo) + assert.Equal(tt.want, m.TableName()) + }) + } +} + +func TestCertificate_clone(t *testing.T) { + t.Parallel() + testCtx := context.Background() + t.Run("valid", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + _, testCertEncoded := TestGenerateCA(t, "localhost") + c, err := NewCertificate(testCtx, "test-id", testCertEncoded) + require.NoError(err) + cp := c.clone() + assert.True(proto.Equal(cp.Certificate, c.Certificate)) + }) + t.Run("not-equal", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + _, testCertEncoded := TestGenerateCA(t, "localhost") + c, err := NewCertificate(testCtx, "test-id", testCertEncoded) + require.NoError(err) + + _, testCertEncoded2 := TestGenerateCA(t, "alice.com") + c2, err := NewCertificate(testCtx, "test-id", testCertEncoded2) + require.NoError(err) + + cp := c.clone() + assert.True(!proto.Equal(cp.Certificate, c2.Certificate)) + }) +} diff --git a/internal/auth/ldap/certificate_utils.go b/internal/auth/ldap/certificate_utils.go new file mode 100644 index 0000000000..b5c56f0ccd --- /dev/null +++ b/internal/auth/ldap/certificate_utils.go @@ -0,0 +1,62 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "bytes" + "context" + "crypto/x509" + "encoding/pem" + "fmt" + + "github.com/hashicorp/boundary/internal/errors" +) + +// EncodeCertificates will encode a number of x509 certificates to PEMs. +func EncodeCertificates(ctx context.Context, certs ...*x509.Certificate) ([]string, error) { + const op = "ldap.EncodeCertificates" + if len(certs) == 0 { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no certs provided") + } + var pems []string + for _, cert := range certs { + if cert == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "nil cert") + } + var buffer bytes.Buffer + err := pem.Encode(&buffer, &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + }) + if err != nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "failed to encode cert: "+err.Error(), errors.WithWrap(err)) + } + pems = append(pems, buffer.String()) + } + return pems, nil +} + +// ParseCertificates will parse a number of certificates PEMs to x509s. +func ParseCertificates(ctx context.Context, pems ...string) ([]*x509.Certificate, error) { + const op = "ldap.ParseCertificates" + if len(pems) == 0 { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no PEMs provided") + } + var certs []*x509.Certificate + for _, p := range pems { + if p == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "empty certificate PEM") + } + block, _ := pem.Decode([]byte(p)) + if block == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "failed to parse certificate: invalid PEM encoding") + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("failed to parse certificate: invalid block: %s", err.Error()), errors.WithWrap(err)) + } + certs = append(certs, cert) + } + return certs, nil +} diff --git a/internal/auth/ldap/certificate_utils_test.go b/internal/auth/ldap/certificate_utils_test.go new file mode 100644 index 0000000000..e71131c0d2 --- /dev/null +++ b/internal/auth/ldap/certificate_utils_test.go @@ -0,0 +1,158 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "crypto/x509" + "testing" + + "github.com/hashicorp/boundary/internal/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEncodeCertificates(t *testing.T) { + ctx := context.TODO() + tests := []struct { + name string + setup func() ([]*x509.Certificate, []string) + wantErr bool + wantErrCode errors.Code + wantErrContains string + }{ + { + name: "valid", + setup: func() ([]*x509.Certificate, []string) { + c1, p1 := TestGenerateCA(t, "localhost") + c2, p2 := TestGenerateCA(t, "alice.com") + + return []*x509.Certificate{c1, c2}, []string{p1, p2} + }, + wantErr: false, + }, + { + name: "empty-cert", + setup: func() ([]*x509.Certificate, []string) { + _, p1 := TestGenerateCA(t, "localhost") + c2, p2 := TestGenerateCA(t, "alice.com") + + return []*x509.Certificate{nil, c2}, []string{p1, p2} + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "nil cert", + }, + { + name: "nil-certs", + setup: func() ([]*x509.Certificate, []string) { + _, p1 := TestGenerateCA(t, "localhost") + _, p2 := TestGenerateCA(t, "alice.com") + + return nil, []string{p1, p2} + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "no certs provided", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + certs, pems := tc.setup() + got, err := EncodeCertificates(ctx, certs...) + if tc.wantErr { + require.Error(err) + assert.True(errors.Match(errors.T(tc.wantErrCode), err)) + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.Equal(pems, got) + }) + } +} + +func TestParseCertificates(t *testing.T) { + ctx := context.TODO() + tests := []struct { + name string + setup func() ([]*x509.Certificate, []string) + wantErr bool + wantErrCode errors.Code + wantErrContains string + }{ + { + name: "valid", + setup: func() ([]*x509.Certificate, []string) { + c1, p1 := TestGenerateCA(t, "localhost") + c2, p2 := TestGenerateCA(t, "alice.com") + + return []*x509.Certificate{c1, c2}, []string{p1, p2} + }, + wantErr: false, + }, + { + name: "empty-pem", + setup: func() ([]*x509.Certificate, []string) { + c1, _ := TestGenerateCA(t, "localhost") + c2, p2 := TestGenerateCA(t, "alice.com") + + return []*x509.Certificate{c1, c2}, []string{"", p2} + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "nil-pem", + setup: func() ([]*x509.Certificate, []string) { + c1, _ := TestGenerateCA(t, "localhost") + c2, _ := TestGenerateCA(t, "alice.com") + + return []*x509.Certificate{c1, c2}, nil + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "invalid-block", + setup: func() ([]*x509.Certificate, []string) { + c1, _ := TestGenerateCA(t, "localhost") + return []*x509.Certificate{c1}, []string{TestInvalidPem} + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "failed to parse certificate: invalid block", + }, + { + name: "invalid-pem", + setup: func() ([]*x509.Certificate, []string) { + c1, _ := TestGenerateCA(t, "localhost") + return []*x509.Certificate{c1}, []string{"not-encoded"} + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "failed to parse certificate: invalid PEM encoding", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + certs, pems := tc.setup() + got, err := ParseCertificates(ctx, pems...) + if tc.wantErr { + require.Error(err) + assert.True(errors.Match(errors.T(tc.wantErrCode), err)) + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.Equal(certs, got) + }) + } +} diff --git a/internal/auth/ldap/client_certificate.go b/internal/auth/ldap/client_certificate.go new file mode 100644 index 0000000000..ffef96b7dc --- /dev/null +++ b/internal/auth/ldap/client_certificate.go @@ -0,0 +1,120 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "crypto/x509" + "encoding/pem" + "fmt" + + "github.com/hashicorp/boundary/internal/auth/ldap/store" + "github.com/hashicorp/boundary/internal/errors" + wrapping "github.com/hashicorp/go-kms-wrapping/v2" + "github.com/hashicorp/go-kms-wrapping/v2/extras/structwrapping" + "google.golang.org/protobuf/proto" +) + +const clientCertificateTableName = "auth_ldap_client_certificate" + +// ClientCertificate represents a set of optional configuration fields used for +// specifying a mTLS client cert for LDAP connections. ClientCertificates are +// value objects of an AuthMethod, therefore there's no need for oplog metadata, +// since only the AuthMethod will have metadata because it's the root aggregate. +type ClientCertificate struct { + *store.ClientCertificate + tableName string +} + +// NewClientCertificate creates a new in memory ClientCertificate. No options +// are currently supported. PrivKey must be in PKCS #8, ASN.1 DER form. certPem +// must be in ASN.1 DER form encoded as PEM. +func NewClientCertificate(ctx context.Context, authMethodId string, privKey []byte, certPem string, _ ...Option) (*ClientCertificate, error) { + const op = "ldap.NewClientCertificate" + switch { + case authMethodId == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id") + case len(privKey) == 0: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing key") + case certPem == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing certificate") + } + if _, err := x509.ParsePKCS8PrivateKey(privKey); err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("failed to parse key in PKCS #8, ASN.1 DER form"), errors.WithCode(errors.InvalidParameter)) + } + blk, _ := pem.Decode([]byte(certPem)) + if blk == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "failed to parse certificate: invalid PEM encoding") + } + if _, err := x509.ParseCertificate(blk.Bytes); err != nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("failed to parse certificate: invalid block: %s", err.Error()), errors.WithWrap(err)) + } + return &ClientCertificate{ + ClientCertificate: &store.ClientCertificate{ + LdapMethodId: authMethodId, + CertificateKey: privKey, + Certificate: []byte(certPem), + }, + }, nil +} + +// allocClientCertificate makes an empty one in memory +func allocClientCertificate() *ClientCertificate { + return &ClientCertificate{ + ClientCertificate: &store.ClientCertificate{}, + } +} + +// clone a ClientCertificate +func (cc *ClientCertificate) clone() *ClientCertificate { + cp := proto.Clone(cc.ClientCertificate) + return &ClientCertificate{ + ClientCertificate: cp.(*store.ClientCertificate), + } +} + +// TableName returns the table name +func (cc *ClientCertificate) TableName() string { + if cc.tableName != "" { + return cc.tableName + } + return clientCertificateTableName +} + +// SetTableName sets the table name. +func (cc *ClientCertificate) SetTableName(n string) { + cc.tableName = n +} + +// encrypt the client certificate before writing it to the database +func (cc *ClientCertificate) encrypt(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "ldap.(ClientCertificate).encrypt" + if cipher == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing cipher") + } + if err := structwrapping.WrapStruct(ctx, cipher, cc.ClientCertificate); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt)) + } + var err error + if cc.KeyId, err = cipher.KeyId(ctx); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("failed to read cipher key id")) + } + if cc.CertificateKeyHmac, err = hmacField(ctx, cipher, cc.CertificateKey, cc.LdapMethodId); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("failed to hmac client certificate")) + } + + return nil +} + +// decrypt the client certificate after reading it from the database +func (cc *ClientCertificate) decrypt(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "ldap.(ClientCertificate).decrypt" + if cipher == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing cipher") + } + if err := structwrapping.UnwrapStruct(ctx, cipher, cc.ClientCertificate); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt)) + } + return nil +} diff --git a/internal/auth/ldap/client_certificate_test.go b/internal/auth/ldap/client_certificate_test.go new file mode 100644 index 0000000000..1884cd5bdb --- /dev/null +++ b/internal/auth/ldap/client_certificate_test.go @@ -0,0 +1,345 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "crypto/ed25519" + "crypto/rand" + "crypto/x509" + "fmt" + "testing" + + "github.com/hashicorp/boundary/internal/auth/ldap/store" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + wrapping "github.com/hashicorp/go-kms-wrapping/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestNewClientCertificate(t *testing.T) { + t.Parallel() + testCtx := context.Background() + _, testCertEncoded := TestGenerateCA(t, "localhost") + _, testPrivKey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + derPrivKey, err := x509.MarshalPKCS8PrivateKey(testPrivKey) + require.NoError(t, err) + + tests := []struct { + name string + ctx context.Context + authMethodId string + certKey []byte + cert string + want *ClientCertificate + wantErr bool + wantErrCode errors.Code + wantErrContains string + }{ + { + name: "valid", + ctx: testCtx, + authMethodId: "test-id", + cert: testCertEncoded, + certKey: derPrivKey, + want: &ClientCertificate{ + ClientCertificate: &store.ClientCertificate{ + LdapMethodId: "test-id", + Certificate: []byte(testCertEncoded), + CertificateKey: derPrivKey, + }, + }, + }, + { + name: "missing-auth-method-id", + ctx: testCtx, + cert: testCertEncoded, + certKey: derPrivKey, + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "missing auth method id", + }, + { + name: "missing-cert", + ctx: testCtx, + authMethodId: "test-id", + certKey: derPrivKey, + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "missing certificate", + }, + { + name: "missing-key", + ctx: testCtx, + authMethodId: "test-id", + cert: testCertEncoded, + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "missing key", + }, + { + name: "invalid-key", + ctx: testCtx, + authMethodId: "test-id", + cert: testCertEncoded, + certKey: []byte("invalid-key"), + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "failed to parse key in PKCS #8, ASN.1 DER form", + }, + { + name: "invalid-block", + ctx: testCtx, + authMethodId: "test-id", + cert: TestInvalidPem, + certKey: derPrivKey, + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "failed to parse certificate: invalid block", + }, + { + name: "invalid-pem", + ctx: testCtx, + authMethodId: "test-id", + cert: "not-encoded", + certKey: derPrivKey, + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "failed to parse certificate: invalid PEM encoding", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := NewClientCertificate(tc.ctx, tc.authMethodId, tc.certKey, tc.cert) + if tc.wantErr { + require.Error(err) + assert.Nil(got) + if tc.wantErrCode != errors.Unknown { + assert.True(errors.Match(errors.T(tc.wantErrCode), err)) + } + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.Equal(tc.want, got) + }) + } +} + +func TestClientCertificate_SetTableName(t *testing.T) { + t.Parallel() + defaultTableName := clientCertificateTableName + tests := []struct { + name string + setNameTo string + want string + }{ + { + name: "new-name", + setNameTo: "new-name", + want: "new-name", + }, + { + name: "reset to default", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + def := allocClientCertificate() + require.Equal(defaultTableName, def.TableName()) + m := allocClientCertificate() + m.SetTableName(tt.setNameTo) + assert.Equal(tt.want, m.TableName()) + }) + } +} + +func TestClientCertificate_clone(t *testing.T) { + t.Parallel() + testCtx := context.Background() + t.Run("valid", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + _, pem := TestGenerateCA(t, "localhost") + _, privKey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(err) + derEncodedKey, err := x509.MarshalPKCS8PrivateKey(privKey) + require.NoError(err) + + cc, err := NewClientCertificate(testCtx, "test-id", derEncodedKey, pem) + require.NoError(err) + cp := cc.clone() + assert.True(proto.Equal(cp.ClientCertificate, cc.ClientCertificate)) + }) + t.Run("not-equal", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + _, pem := TestGenerateCA(t, "localhost") + _, privKey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(err) + derEncodedKey, err := x509.MarshalPKCS8PrivateKey(privKey) + require.NoError(err) + cc, err := NewClientCertificate(testCtx, "test-id", derEncodedKey, pem) + require.NoError(err) + + _, pem2 := TestGenerateCA(t, "localhost") + _, privKey2, err := ed25519.GenerateKey(rand.Reader) + require.NoError(err) + derEncodedKey2, err := x509.MarshalPKCS8PrivateKey(privKey2) + require.NoError(err) + cc2, err := NewClientCertificate(testCtx, "test-id", derEncodedKey2, pem2) + require.NoError(err) + + cp := cc.clone() + assert.True(!proto.Equal(cp.ClientCertificate, cc2.ClientCertificate)) + }) +} + +func TestClientCertificate_encrypt_decrypt(t *testing.T) { + t.Parallel() + testCtx := context.Background() + testWrapper := db.TestWrapper(t) + _, testCertEncoded := TestGenerateCA(t, "localhost") + _, testPrivKey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + derPrivKey, err := x509.MarshalPKCS8PrivateKey(testPrivKey) + require.NoError(t, err) + + tests := []struct { + name string + ctx context.Context + cipher wrapping.Wrapper + cc *ClientCertificate + wantErr bool + wantErrCode errors.Code + wantErrContains string + }{ + { + name: "valid", + ctx: testCtx, + cipher: testWrapper, + cc: func() *ClientCertificate { + testClientCert, err := NewClientCertificate(testCtx, "test-auth-method-id", derPrivKey, testCertEncoded) + require.NoError(t, err) + return testClientCert + }(), + }, + { + name: "missing-cipher", + ctx: testCtx, + cc: func() *ClientCertificate { + testClientCert, err := NewClientCertificate(testCtx, "test-auth-method-id", derPrivKey, testCertEncoded) + require.NoError(t, err) + return testClientCert + }(), + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "missing cipher", + }, + { + name: "encrypt-err", + ctx: testCtx, + cipher: &kms.MockWrapper{ + Wrapper: testWrapper, + EncryptErr: fmt.Errorf("test encrypt error"), + }, + cc: func() *ClientCertificate { + testClientCert, err := NewClientCertificate(testCtx, "test-auth-method-id", derPrivKey, testCertEncoded) + require.NoError(t, err) + return testClientCert + }(), + wantErr: true, + wantErrCode: errors.Encrypt, + wantErrContains: "test encrypt error", + }, + { + name: "keyId-err", + ctx: testCtx, + cipher: &kms.MockWrapper{ + Wrapper: testWrapper, + KeyIdErr: fmt.Errorf("test key id error"), + }, + cc: func() *ClientCertificate { + testClientCert, err := NewClientCertificate(testCtx, "test-auth-method-id", derPrivKey, testCertEncoded) + require.NoError(t, err) + return testClientCert + }(), + wantErr: true, + wantErrCode: errors.Encrypt, + wantErrContains: "test key id error", + }, + { + name: "keyBytes-err", + ctx: testCtx, + cipher: &kms.MockWrapper{ + Wrapper: testWrapper, + // KeyBytesErr: fmt.Errorf("test key bytes error"), + }, + cc: func() *ClientCertificate { + testClientCert, err := NewClientCertificate(testCtx, "test-auth-method-id", derPrivKey, testCertEncoded) + require.NoError(t, err) + return testClientCert + }(), + wantErr: true, + wantErrCode: errors.Encrypt, + wantErrContains: "failed to hmac client certificate", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + require.Empty(tc.cc.CtCertificateKey) + require.Empty(tc.cc.CertificateKeyHmac) + + err := tc.cc.encrypt(tc.ctx, tc.cipher) + if tc.wantErr { + require.Error(err) + if tc.wantErrCode != errors.Unknown { + assert.True(errors.Match(errors.T(tc.wantErrCode), err)) + } + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.NotEmpty(tc.cc.GetCertificateKeyHmac()) + assert.NotEmpty(tc.cc.GetCtCertificateKey()) + + origKey := make([]byte, len(tc.cc.CertificateKey)) + copy(origKey, tc.cc.CertificateKey) + tc.cc.CertificateKey = nil + require.NoError(tc.cc.decrypt(tc.ctx, tc.cipher)) + assert.NotEmpty(tc.cc.CertificateKey) + assert.Equal(origKey, tc.cc.CertificateKey) + }) + } + t.Run("decrypt-missing-cipher", func(t *testing.T) { + testBindCred, err := NewClientCertificate(testCtx, "test-id", derPrivKey, testCertEncoded) + require.NoError(t, err) + err = testBindCred.decrypt(testCtx, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "missing cipher") + }) + t.Run("decrypt-err", func(t *testing.T) { + w := &kms.MockWrapper{ + Wrapper: testWrapper, + DecryptErr: fmt.Errorf("test decrypt error"), + } + testBindCred, err := NewClientCertificate(testCtx, "test-id", derPrivKey, testCertEncoded) + require.NoError(t, err) + require.NoError(t, testBindCred.encrypt(testCtx, testWrapper)) + err = testBindCred.decrypt(testCtx, w) + require.Error(t, err) + assert.Contains(t, err.Error(), "test decrypt error") + }) +} diff --git a/internal/auth/ldap/group_entry_search_conf.go b/internal/auth/ldap/group_entry_search_conf.go new file mode 100644 index 0000000000..822839288d --- /dev/null +++ b/internal/auth/ldap/group_entry_search_conf.go @@ -0,0 +1,78 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + + "github.com/hashicorp/boundary/internal/auth/ldap/store" + "github.com/hashicorp/boundary/internal/errors" + "google.golang.org/protobuf/proto" +) + +const groupEntrySearchConfTableName = "auth_ldap_group_entry_search" + +// GroupEntrySearchConf represent a set of optional configuration fields used to +// search for group entries. It is assigned to an LDAP AuthMethod and +// updates/deletes to that AuthMethod are cascaded to its GroupEntrySearchConf. +// GroupEntrySearchConf are value objects of an AuthMethod, therefore there's no +// need for oplog metadata, since only the AuthMethod will have metadata because +// it's the root aggregate. +type GroupEntrySearchConf struct { + *store.GroupEntrySearchConf + tableName string +} + +// NewGroupEntrySearchConf creates a new in memory NewGroupEntrySearchConf. +// Supported options are: WithGroupDn, WithGroupAttr, WithGroupFilter and all +// other options are ignored. +func NewGroupEntrySearchConf(ctx context.Context, authMethodId string, opt ...Option) (*GroupEntrySearchConf, error) { + const op = "ldap.NewGroupEntrySearchConf" + opts, err := getOpts(opt...) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + switch { + case authMethodId == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id") + case opts.withGroupDn == "" && opts.withGroupAttr == "" && opts.withGroupFilter == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "you must supply either dn, attr, or filter") + } + return &GroupEntrySearchConf{ + GroupEntrySearchConf: &store.GroupEntrySearchConf{ + LdapMethodId: authMethodId, + GroupDn: opts.withGroupDn, + GroupAttr: opts.withGroupAttr, + GroupFilter: opts.withGroupFilter, + }, + }, nil +} + +// allocGroupEntrySearchConf makes an empty one in memory +func allocGroupEntrySearchConf() *GroupEntrySearchConf { + return &GroupEntrySearchConf{ + GroupEntrySearchConf: &store.GroupEntrySearchConf{}, + } +} + +// clone a GroupEntrySearchConf +func (gc *GroupEntrySearchConf) clone() *GroupEntrySearchConf { + cp := proto.Clone(gc.GroupEntrySearchConf) + return &GroupEntrySearchConf{ + GroupEntrySearchConf: cp.(*store.GroupEntrySearchConf), + } +} + +// TableName returns the table name +func (gc *GroupEntrySearchConf) TableName() string { + if gc.tableName != "" { + return gc.tableName + } + return groupEntrySearchConfTableName +} + +// SetTableName sets the table name. +func (gc *GroupEntrySearchConf) SetTableName(n string) { + gc.tableName = n +} diff --git a/internal/auth/ldap/group_entry_search_conf_test.go b/internal/auth/ldap/group_entry_search_conf_test.go new file mode 100644 index 0000000000..a8cec96e09 --- /dev/null +++ b/internal/auth/ldap/group_entry_search_conf_test.go @@ -0,0 +1,180 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/auth/ldap/store" + "github.com/hashicorp/boundary/internal/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestNewGroupEntrySearchConf(t *testing.T) { + t.Parallel() + testCtx := context.Background() + tests := []struct { + name string + ctx context.Context + authMethodId string + opts []Option + want *GroupEntrySearchConf + wantErr bool + wantErrCode errors.Code + wantErrContains string + }{ + { + name: "valid", + ctx: testCtx, + authMethodId: "test-id", + opts: []Option{ + WithGroupDn(testCtx, "dn"), + WithGroupAttr(testCtx, "attr"), + WithGroupFilter(testCtx, "filter"), + }, + want: &GroupEntrySearchConf{ + GroupEntrySearchConf: &store.GroupEntrySearchConf{ + LdapMethodId: "test-id", + GroupDn: "dn", + GroupAttr: "attr", + GroupFilter: "filter", + }, + }, + }, + { + name: "just-dn", + ctx: testCtx, + authMethodId: "test-id", + opts: []Option{ + WithGroupDn(testCtx, "dn"), + }, + want: &GroupEntrySearchConf{ + GroupEntrySearchConf: &store.GroupEntrySearchConf{ + LdapMethodId: "test-id", + GroupDn: "dn", + }, + }, + }, + { + name: "just-attr", + ctx: testCtx, + authMethodId: "test-id", + opts: []Option{ + WithGroupAttr(testCtx, "attr"), + }, + want: &GroupEntrySearchConf{ + GroupEntrySearchConf: &store.GroupEntrySearchConf{ + LdapMethodId: "test-id", + GroupAttr: "attr", + }, + }, + }, + { + name: "just-filter", + ctx: testCtx, + authMethodId: "test-id", + opts: []Option{ + WithGroupFilter(testCtx, "filter"), + }, + want: &GroupEntrySearchConf{ + GroupEntrySearchConf: &store.GroupEntrySearchConf{ + LdapMethodId: "test-id", + GroupFilter: "filter", + }, + }, + }, + { + name: "missing-auth-method-id", + ctx: testCtx, + opts: []Option{WithGroupDn(testCtx, "dn")}, + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "missing auth method id", + }, + { + name: "no-opts", + ctx: testCtx, + authMethodId: "test-id", + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "you must supply either dn, attr, or filter", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := NewGroupEntrySearchConf(tc.ctx, tc.authMethodId, tc.opts...) + if tc.wantErr { + require.Error(err) + assert.Nil(got) + if tc.wantErrCode != errors.Unknown { + assert.True(errors.Match(errors.T(tc.wantErrCode), err)) + } + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.Equal(tc.want, got) + }) + } +} + +func TestGroupEntrySearchConf_SetTableName(t *testing.T) { + t.Parallel() + defaultTableName := groupEntrySearchConfTableName + tests := []struct { + name string + setNameTo string + want string + }{ + { + name: "new-name", + setNameTo: "new-name", + want: "new-name", + }, + { + name: "reset to default", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + def := allocGroupEntrySearchConf() + require.Equal(defaultTableName, def.TableName()) + m := allocGroupEntrySearchConf() + m.SetTableName(tt.setNameTo) + assert.Equal(tt.want, m.TableName()) + }) + } +} + +func TestGroupEntrySearchConf_clone(t *testing.T) { + t.Parallel() + testCtx := context.Background() + t.Run("valid", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + uc, err := NewGroupEntrySearchConf(testCtx, "test-id", WithGroupDn(testCtx, "dn")) + require.NoError(err) + cp := uc.clone() + assert.True(proto.Equal(cp.GroupEntrySearchConf, uc.GroupEntrySearchConf)) + }) + t.Run("not-equal", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + uc, err := NewGroupEntrySearchConf(testCtx, "test-id", WithGroupDn(testCtx, "dn")) + require.NoError(err) + + uc2, err := NewGroupEntrySearchConf(testCtx, "test-id", WithGroupDn(testCtx, "dn2")) + require.NoError(err) + + cp := uc.clone() + assert.True(!proto.Equal(cp.GroupEntrySearchConf, uc2.GroupEntrySearchConf)) + }) +} diff --git a/internal/auth/ldap/ids.go b/internal/auth/ldap/ids.go new file mode 100644 index 0000000000..2d5dd7764b --- /dev/null +++ b/internal/auth/ldap/ids.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/auth" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/types/subtypes" +) + +func init() { + if err := subtypes.Register(auth.Domain, Subtype, globals.LdapAuthMethodPrefix, globals.LdapAccountPrefix, globals.LdapManagedGroupPrefix); err != nil { + panic(err) + } +} + +const ( + Subtype = subtypes.Subtype("ldap") +) + +func newAuthMethodId(ctx context.Context) (string, error) { + const op = "ldap.newAuthMethodId" + id, err := db.NewPublicId(globals.LdapAuthMethodPrefix) + if err != nil { + return "", errors.Wrap(ctx, err, op) + } + return id, nil +} + +func newAccountId(ctx context.Context, authMethodId, loginName string) (string, error) { + const op = "ldap.newAccountId" + // there's a unique index on: auth method id + login name + id, err := db.NewPublicId(globals.LdapAccountPrefix, db.WithPrngValues([]string{authMethodId, loginName})) + if err != nil { + return "", errors.Wrap(ctx, err, op) + } + return id, nil +} + +func newManagedGroupId(ctx context.Context) (string, error) { + const op = "ldap.newManagedGroupId" + id, err := db.NewPublicId(globals.LdapManagedGroupPrefix) + if err != nil { + return "", errors.Wrap(ctx, err, op) + } + return id, nil +} diff --git a/internal/auth/ldap/managed_group.go b/internal/auth/ldap/managed_group.go new file mode 100644 index 0000000000..e93b85bb69 --- /dev/null +++ b/internal/auth/ldap/managed_group.go @@ -0,0 +1,106 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "encoding/json" + + "github.com/hashicorp/boundary/internal/auth/ldap/store" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/oplog" + "google.golang.org/protobuf/proto" +) + +// managedGroupTableName defines the default table name for a Managed Group +const managedGroupTableName = "auth_ldap_managed_group" + +// ManagedGroup contains an LDAP managed group. It is assigned to an LDAP AuthMethod +// and updates/deletes to that AuthMethod are cascaded to its Managed Groups. +type ManagedGroup struct { + *store.ManagedGroup + tableName string +} + +// NewManagedGroup creates a new in memory ManagedGroup assigned to LDAP +// AuthMethod. Supported options are WithName and WithDescription. +func NewManagedGroup(ctx context.Context, authMethodId string, groupNames []string, opt ...Option) (*ManagedGroup, error) { + const op = "ldap.NewManagedGroup" + switch { + case authMethodId == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id") + case len(groupNames) == 0: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing group names") + } + opts, err := getOpts(opt...) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + n, err := json.Marshal(groupNames) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to marshal group names")) + } + mg := &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + AuthMethodId: authMethodId, + Name: opts.withName, + Description: opts.withDescription, + GroupNames: string(n), + }, + } + return mg, nil +} + +// AllocManagedGroup makes an empty one in memory +func AllocManagedGroup() *ManagedGroup { + return &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{}, + } +} + +// clone a ManagedGroup. +func (mg *ManagedGroup) clone() *ManagedGroup { + cp := proto.Clone(mg.ManagedGroup) + return &ManagedGroup{ + ManagedGroup: cp.(*store.ManagedGroup), + } +} + +// TableName returns the table name. +func (mg *ManagedGroup) TableName() string { + if mg.tableName != "" { + return mg.tableName + } + return managedGroupTableName +} + +// SetTableName sets the table name. +func (mg *ManagedGroup) SetTableName(n string) { + mg.tableName = n +} + +// oplog will create oplog metadata for the ManagedGroup. +func (mg *ManagedGroup) oplog(ctx context.Context, opType oplog.OpType, authMethodScopeId string) (oplog.Metadata, error) { + const op = "ldap.(ManagedGroup).oplog" + switch { + case mg == nil: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing managed group") + case opType == oplog.OpType_OP_TYPE_UNSPECIFIED: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing op type") + case mg.PublicId == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing public id") + case mg.AuthMethodId == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id") + case authMethodScopeId == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope id") + } + metadata := oplog.Metadata{ + "resource-public-id": []string{mg.PublicId}, + "resource-type": []string{"ldap managed group"}, + "op-type": []string{opType.String()}, + "scope-id": []string{authMethodScopeId}, + "auth-method-id": []string{mg.AuthMethodId}, + } + return metadata, nil +} diff --git a/internal/auth/ldap/managed_group_test.go b/internal/auth/ldap/managed_group_test.go new file mode 100644 index 0000000000..1176f4eef2 --- /dev/null +++ b/internal/auth/ldap/managed_group_test.go @@ -0,0 +1,222 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/auth/ldap/store" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewManagedGroup(t *testing.T) { + t.Parallel() + testCtx := context.Background() + tests := []struct { + name string + ctx context.Context + authMethodId string + groupNames []string + opt []Option + want *ManagedGroup + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "success", + ctx: testCtx, + authMethodId: "test-auth-method-id", + groupNames: []string{"admin"}, + opt: []Option{WithName(testCtx, "success"), WithDescription(testCtx, "description")}, + want: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "success", + Description: "description", + AuthMethodId: "test-auth-method-id", + GroupNames: TestEncodedGrpNames(t, "admin"), + }, + }, + }, + { + name: "missing-auth-method-id", + ctx: testCtx, + groupNames: []string{"admin"}, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing auth method id", + }, + { + name: "missing-group-names", + ctx: testCtx, + authMethodId: "test-auth-method-id", + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing group names", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := NewManagedGroup(tc.ctx, tc.authMethodId, tc.groupNames, tc.opt...) + if tc.wantErrMatch != nil { + require.Error(err) + assert.Nil(got) + assert.True(errors.Match(tc.wantErrMatch, err)) + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.Equal(tc.want, got) + }) + } +} + +func TestManagedGroup_SetTableName(t *testing.T) { + t.Parallel() + defaultTableName := managedGroupTableName + tests := []struct { + name string + setNameTo string + want string + }{ + { + name: "new-name", + setNameTo: "new-name", + want: "new-name", + }, + { + name: "reset to default", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + def := AllocManagedGroup() + require.Equal(defaultTableName, def.TableName()) + m := AllocManagedGroup() + m.SetTableName(tc.setNameTo) + assert.Equal(tc.want, m.TableName()) + }) + } +} + +func TestManagedGroup_oplog(t *testing.T) { + t.Parallel() + testCtx := context.Background() + testMg, err := NewManagedGroup(testCtx, "test-id", []string{"admin"}) + testMg.PublicId = "test-public-id" + require.NoError(t, err) + tests := []struct { + name string + ctx context.Context + mg *ManagedGroup + opType oplog.OpType + scopeId string + want oplog.Metadata + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "create", + ctx: testCtx, + mg: testMg, + opType: oplog.OpType_OP_TYPE_CREATE, + scopeId: "global", + want: oplog.Metadata{ + "auth-method-id": {"test-id"}, + "resource-public-id": {"test-public-id"}, + "scope-id": {"global"}, + "op-type": {oplog.OpType_OP_TYPE_CREATE.String()}, + "resource-type": {"ldap managed group"}, + }, + }, + { + name: "update", + ctx: testCtx, + mg: testMg, + opType: oplog.OpType_OP_TYPE_UPDATE, + scopeId: "global", + want: oplog.Metadata{ + "auth-method-id": {"test-id"}, + "resource-public-id": {"test-public-id"}, + "scope-id": {"global"}, + "op-type": {oplog.OpType_OP_TYPE_UPDATE.String()}, + "resource-type": {"ldap managed group"}, + }, + }, + { + name: "missing-auth-method-id", + ctx: testCtx, + mg: func() *ManagedGroup { + cp := testMg.clone() + cp.AuthMethodId = "" + return cp + }(), + opType: oplog.OpType_OP_TYPE_UPDATE, + scopeId: "global", + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing auth method id", + }, + { + name: "missing-scope-id", + ctx: testCtx, + mg: testMg, + opType: oplog.OpType_OP_TYPE_UPDATE, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing scope id", + }, + { + name: "missing-public-id", + ctx: testCtx, + mg: func() *ManagedGroup { + cp := testMg.clone() + cp.PublicId = "" + return cp + }(), + opType: oplog.OpType_OP_TYPE_UPDATE, + scopeId: "global", + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing public id", + }, + { + name: "missing-op-type", + ctx: testCtx, + mg: testMg, + scopeId: "global", + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing op type", + }, + { + name: "missing-managed-group", + ctx: testCtx, + opType: oplog.OpType_OP_TYPE_UPDATE, + scopeId: "global", + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing managed group", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := tc.mg.oplog(tc.ctx, tc.opType, tc.scopeId) + if tc.wantErrMatch != nil { + require.Error(err) + assert.Nil(got) + assert.True(errors.Match(tc.wantErrMatch, err)) + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.Equal(tc.want, got) + }) + } +} diff --git a/internal/auth/ldap/options.go b/internal/auth/ldap/options.go new file mode 100644 index 0000000000..f4bc7b3f9e --- /dev/null +++ b/internal/auth/ldap/options.go @@ -0,0 +1,365 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "crypto/x509" + "encoding/json" + "fmt" + "net/url" + + "github.com/hashicorp/boundary/internal/errors" +) + +type options struct { + withName string + withDescription string + withFullName string + withEmail string + withDn string + withStartTls bool + withInsecureTls bool + withDiscoverDn bool + withAnonGroupSearch bool + withEnableGroups bool + withUseTokenGroups bool + withUpnDomain string + withUserDn string + withUserAttr string + withUserFilter string + withGroupDn string + withGroupAttr string + withGroupFilter string + withCertificates []string + withBindDn string + withBindPassword string + withClientCertificate string + withClientCertificateKey []byte + withLimit int + withUnauthenticatedUser bool + withOrderByCreateTime bool + ascending bool + withOperationalState AuthMethodState + withAccountAttributeMap map[string]AccountToAttribute + withMemberOfGroups string + withUrls []string +} + +// Option - how options are passed as args +type Option func(*options) error + +func getDefaultOptions() options { + return options{ + withOperationalState: InactiveState, + } +} + +func getOpts(opt ...Option) (options, error) { + opts := getDefaultOptions() + + for _, o := range opt { + if err := o(&opts); err != nil { + return opts, err + } + } + return opts, nil +} + +// WithUrls provides optional urls for the auth method. +func WithUrls(ctx context.Context, urls ...*url.URL) Option { + const op = "ldap.WithUrls" + return func(o *options) error { + o.withUrls = make([]string, 0, len(urls)) + for _, u := range urls { + switch u.Scheme { + case "ldap", "ldaps": + default: + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("%s scheme in url %q is not either ldap or ldaps", u.Scheme, u.String())) + } + o.withUrls = append(o.withUrls, u.String()) + } + return nil + } +} + +// WithEmail provides an optional email address for the account. +func WithEmail(_ context.Context, email string) Option { + return func(o *options) error { + o.withEmail = email + return nil + } +} + +// WithFullName provides an optional full name for the account. +func WithFullName(_ context.Context, n string) Option { + return func(o *options) error { + o.withFullName = n + return nil + } +} + +// WithDn provides an optional distinguished name +func WithDn(ctx context.Context, dn string) Option { + const op = "ldap.WithDn" + return func(o *options) error { + o.withDn = dn + return nil + } +} + +// WithName provides an optional name. +func WithName(_ context.Context, n string) Option { + return func(o *options) error { + o.withName = n + return nil + } +} + +// WithDescription provides an optional description. +func WithDescription(_ context.Context, desc string) Option { + return func(o *options) error { + o.withDescription = desc + return nil + } +} + +// WithStartTLS optionally enables a StartTLS command after establishing an +// unencrypted connection. +func WithStartTLS(_ context.Context) Option { + return func(o *options) error { + o.withStartTls = true + return nil + } +} + +// WithEnableGroups optionally enables an authenticated user's groups will be +// found during authentication. +func WithEnableGroups(_ context.Context) Option { + return func(o *options) error { + o.withEnableGroups = true + return nil + } +} + +// WithUseTokenGroups optionally enables the use the Active Directory +// tokenGroups constructed attribute of the user to find the group memberships. +// This will find all security groups including nested ones, +func WithUseTokenGroups(_ context.Context) Option { + return func(o *options) error { + o.withUseTokenGroups = true + return nil + } +} + +// WithInsecureTLS optional specifies to skip LDAP server SSL certificate +// validation - insecure and use with caution +func WithInsecureTLS(_ context.Context) Option { + return func(o *options) error { + o.withInsecureTls = true + return nil + } +} + +// WithDiscoverDn optionally specifies to use anon bind to discover the bind DN +// of a user. +func WithDiscoverDn(_ context.Context) Option { + return func(o *options) error { + o.withDiscoverDn = true + return nil + } +} + +// WithAnonGroupSearch optionally specifies to use anon bind when performing LDAP +// group searches +func WithAnonGroupSearch(_ context.Context) Option { + return func(o *options) error { + o.withAnonGroupSearch = true + return nil + } +} + +// WithUpnDomain optionally specifies the userPrincipalDomain used to construct +// the UPN string for the authenticating user. The constructed UPN will appear +// as [username]@UPNDomain Example: example.com, which will cause Boundary to +// bind as username@example.com when authenticating the user. +func WithUpnDomain(_ context.Context, domain string) Option { + return func(o *options) error { + o.withUpnDomain = domain + return nil + } +} + +// WithUserDn optionally specifies a user dn used to search for user entries. +func WithUserDn(_ context.Context, dn string) Option { + return func(o *options) error { + o.withUserDn = dn + return nil + } +} + +// WithUserAttr optionally specifies a user attr used to search for user entries. +func WithUserAttr(_ context.Context, attr string) Option { + return func(o *options) error { + o.withUserAttr = attr + return nil + } +} + +// WithUserFilter optionally specifies a user filter used to search for user entries. +func WithUserFilter(_ context.Context, filter string) Option { + return func(o *options) error { + o.withUserFilter = filter + return nil + } +} + +// WithGroupDn optionally specifies a group dn used to search for group entries. +func WithGroupDn(_ context.Context, dn string) Option { + return func(o *options) error { + o.withGroupDn = dn + return nil + } +} + +// WithGroupAttr optionally specifies a group attr used to search for group entries. +func WithGroupAttr(_ context.Context, attr string) Option { + return func(o *options) error { + o.withGroupAttr = attr + return nil + } +} + +// WithGroupFilter optionally specifies a group filter used to search for group entries. +func WithGroupFilter(_ context.Context, filter string) Option { + return func(o *options) error { + o.withGroupFilter = filter + return nil + } +} + +// WithBindCredential optionally specifies a set of optional configuration +// parameters which allow Boundary to bind (aka authenticate) using the +// credentials provided when searching for the user entry used to authenticate +// the end user. +func WithBindCredential(ctx context.Context, dn, password string) Option { + const op = "ldap.WithBindCredential" + return func(o *options) error { + switch { + case dn == "" && password == "": + return errors.New(ctx, errors.InvalidParameter, op, "missing both dn and password") + case dn != "" && password == "": + return errors.New(ctx, errors.InvalidParameter, op, "missing password") + case dn == "" && password != "": + return errors.New(ctx, errors.InvalidParameter, op, "missing dn") + } + o.withBindDn = dn + o.withBindPassword = password + return nil + } +} + +// WithCertificates provides optional certificates. +func WithCertificates(ctx context.Context, certs ...*x509.Certificate) Option { + const op = "ldap.WithCertificates" + return func(o *options) error { + if len(certs) > 0 { + o.withCertificates = make([]string, 0, len(certs)) + pem, err := EncodeCertificates(ctx, certs...) + if err != nil { + return errors.Wrap(ctx, err, op) + } + o.withCertificates = append(o.withCertificates, pem...) + } + return nil + } +} + +// WithClientCertificate provides optional configuration fields used for +// specifying a mTLS client cert for LDAP connections. +func WithClientCertificate(ctx context.Context, privKey []byte, cert *x509.Certificate) Option { + const op = "ldap.WithClientCertificate" + return func(o *options) error { + if privKey != nil || cert != nil { + switch { + case cert == nil: + return errors.New(ctx, errors.InvalidParameter, op, "missing certificate") + case len(privKey) == 0: + return errors.New(ctx, errors.InvalidParameter, op, "missing private key") + } + if _, err := x509.ParsePKCS8PrivateKey(privKey); err != nil { + return errors.Wrap(ctx, err, op) + } + o.withClientCertificateKey = privKey + pem, err := EncodeCertificates(ctx, cert) + if err != nil { + return errors.Wrap(ctx, err, op) + } + if len(pem) != 1 { + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("too many client certificates (%d)", len(pem))) + } + o.withClientCertificate = pem[0] + } + return nil + } +} + +// WithLimit provides an option to provide a limit. Intentionally allowing +// negative integers. If WithLimit < 0, then unlimited results are returned. +// If WithLimit == 0, then default limits are used for results. +func WithLimit(_ context.Context, l int) Option { + return func(o *options) error { + o.withLimit = l + return nil + } +} + +// WithUnauthenticatedUser provides an option for filtering results for +// an unauthenticated users. +func WithUnauthenticatedUser(_ context.Context, enabled bool) Option { + return func(o *options) error { + o.withUnauthenticatedUser = enabled + return nil + } +} + +// WithOrderByCreateTime provides an option to specify ordering by the +// CreateTime field. +func WithOrderByCreateTime(_ context.Context, ascending bool) Option { + return func(o *options) error { + o.withOrderByCreateTime = true + o.ascending = ascending + return nil + } +} + +// WithOperationalState provides an option for specifying the auth method's +// operational state +func WithOperationalState(_ context.Context, state AuthMethodState) Option { + return func(o *options) error { + o.withOperationalState = state + return nil + } +} + +// WithAccountAttributeMap provides an option for specifying an Account Attribute map. +func WithAccountAttributeMap(_ context.Context, aam map[string]AccountToAttribute) Option { + return func(o *options) error { + o.withAccountAttributeMap = aam + return nil + } +} + +// WithMemberOfGroups provides an option for specifying a list of group names +func WithMemberOfGroups(ctx context.Context, groupName ...string) Option { + const op = "ldap.WithMemberOfGroups" + return func(o *options) error { + mg, err := json.Marshal(groupName) + if err != nil { + return errors.Wrap(ctx, err, op) + } + o.withMemberOfGroups = string(mg) + return nil + } +} diff --git a/internal/auth/ldap/options_test.go b/internal/auth/ldap/options_test.go new file mode 100644 index 0000000000..21483ef795 --- /dev/null +++ b/internal/auth/ldap/options_test.go @@ -0,0 +1,318 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "crypto/ed25519" + "crypto/rand" + "crypto/x509" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_getOpts(t *testing.T) { + t.Parallel() + testCtx := context.Background() + t.Run("WithName", func(t *testing.T) { + assert := assert.New(t) + opts, err := getOpts(WithName(testCtx, "test")) + require.NoError(t, err) + testOpts := getDefaultOptions() + assert.NotEqual(opts, testOpts) + testOpts.withName = "test" + assert.Equal(opts, testOpts) + }) + t.Run("WithDescription", func(t *testing.T) { + assert := assert.New(t) + opts, err := getOpts(WithDescription(testCtx, "test")) + require.NoError(t, err) + testOpts := getDefaultOptions() + assert.NotEqual(opts, testOpts) + testOpts.withDescription = "test" + assert.Equal(opts, testOpts) + }) + t.Run("WithUrls", func(t *testing.T) { + assert := assert.New(t) + opts, err := getOpts(WithUrls(testCtx, TestConvertToUrls(t, "ldaps://ldap1")...)) + require.NoError(t, err) + testOpts := getDefaultOptions() + assert.NotEqual(opts, testOpts) + testOpts.withUrls = []string{"ldaps://ldap1"} + assert.Equal(opts, testOpts) + }) + t.Run("WithAccountAttributeMap", func(t *testing.T) { + assert := assert.New(t) + opts, err := getOpts(WithAccountAttributeMap(testCtx, map[string]AccountToAttribute{ + "mail": ToEmailAttribute, + })) + require.NoError(t, err) + testOpts := getDefaultOptions() + assert.NotEqual(opts, testOpts) + testOpts.withAccountAttributeMap = map[string]AccountToAttribute{ + "mail": ToEmailAttribute, + } + assert.Equal(opts, testOpts) + }) + t.Run("WithStartTLS", func(t *testing.T) { + assert := assert.New(t) + opts, err := getOpts(WithStartTLS(testCtx)) + require.NoError(t, err) + testOpts := getDefaultOptions() + assert.NotEqual(opts, testOpts) + testOpts.withStartTls = true + assert.Equal(opts, testOpts) + }) + t.Run("WithInsecureTLS", func(t *testing.T) { + assert := assert.New(t) + opts, err := getOpts(WithInsecureTLS(testCtx)) + require.NoError(t, err) + testOpts := getDefaultOptions() + assert.NotEqual(opts, testOpts) + testOpts.withInsecureTls = true + assert.Equal(opts, testOpts) + }) + t.Run("WithDiscoverDn", func(t *testing.T) { + assert := assert.New(t) + opts, err := getOpts(WithDiscoverDn(testCtx)) + require.NoError(t, err) + testOpts := getDefaultOptions() + assert.NotEqual(opts, testOpts) + testOpts.withDiscoverDn = true + assert.Equal(opts, testOpts) + }) + t.Run("WithAnonGroupSearch", func(t *testing.T) { + assert := assert.New(t) + opts, err := getOpts(WithAnonGroupSearch(testCtx)) + require.NoError(t, err) + testOpts := getDefaultOptions() + assert.NotEqual(opts, testOpts) + testOpts.withAnonGroupSearch = true + assert.Equal(opts, testOpts) + }) + t.Run("WithEnableGroups", func(t *testing.T) { + assert := assert.New(t) + opts, err := getOpts(WithEnableGroups(testCtx)) + require.NoError(t, err) + testOpts := getDefaultOptions() + assert.NotEqual(opts, testOpts) + testOpts.withEnableGroups = true + assert.Equal(opts, testOpts) + }) + t.Run("WithUseTokenGroups", func(t *testing.T) { + assert := assert.New(t) + opts, err := getOpts(WithUseTokenGroups(testCtx)) + require.NoError(t, err) + testOpts := getDefaultOptions() + assert.NotEqual(opts, testOpts) + testOpts.withUseTokenGroups = true + assert.Equal(opts, testOpts) + }) + t.Run("WithUpnDomain", func(t *testing.T) { + assert := assert.New(t) + opts, err := getOpts(WithUpnDomain(testCtx, "domain.com")) + require.NoError(t, err) + testOpts := getDefaultOptions() + assert.NotEqual(opts, testOpts) + testOpts.withUpnDomain = "domain.com" + assert.Equal(opts, testOpts) + }) + t.Run("WitUserDn", func(t *testing.T) { + assert := assert.New(t) + opts, err := getOpts(WithUserDn(testCtx, "dn")) + require.NoError(t, err) + testOpts := getDefaultOptions() + assert.NotEqual(opts, testOpts) + testOpts.withUserDn = "dn" + assert.Equal(opts, testOpts) + }) + t.Run("WitUserAttr", func(t *testing.T) { + assert := assert.New(t) + opts, err := getOpts(WithUserAttr(testCtx, "attr")) + require.NoError(t, err) + testOpts := getDefaultOptions() + assert.NotEqual(opts, testOpts) + testOpts.withUserAttr = "attr" + assert.Equal(opts, testOpts) + }) + t.Run("WitUserFilter", func(t *testing.T) { + assert := assert.New(t) + opts, err := getOpts(WithUserFilter(testCtx, "filter")) + require.NoError(t, err) + testOpts := getDefaultOptions() + assert.NotEqual(opts, testOpts) + testOpts.withUserFilter = "filter" + assert.Equal(opts, testOpts) + }) + t.Run("WitGroupDn", func(t *testing.T) { + assert := assert.New(t) + opts, err := getOpts(WithGroupDn(testCtx, "dn")) + require.NoError(t, err) + testOpts := getDefaultOptions() + assert.NotEqual(opts, testOpts) + testOpts.withGroupDn = "dn" + assert.Equal(opts, testOpts) + }) + t.Run("WithGroupAttr", func(t *testing.T) { + assert := assert.New(t) + opts, err := getOpts(WithGroupAttr(testCtx, "attr")) + require.NoError(t, err) + testOpts := getDefaultOptions() + assert.NotEqual(opts, testOpts) + testOpts.withGroupAttr = "attr" + assert.Equal(opts, testOpts) + }) + t.Run("WithGroupFilter", func(t *testing.T) { + assert := assert.New(t) + opts, err := getOpts(WithGroupFilter(testCtx, "filter")) + require.NoError(t, err) + testOpts := getDefaultOptions() + assert.NotEqual(opts, testOpts) + testOpts.withGroupFilter = "filter" + assert.Equal(opts, testOpts) + }) + t.Run("WithBindCredential", func(t *testing.T) { + assert := assert.New(t) + opts, err := getOpts(WithBindCredential(testCtx, "dn", "password")) + require.NoError(t, err) + testOpts := getDefaultOptions() + assert.NotEqual(opts, testOpts) + testOpts.withBindDn = "dn" + testOpts.withBindPassword = "password" + assert.Equal(opts, testOpts) + }) + t.Run("WithBindCredential-missing-dn", func(t *testing.T) { + assert := assert.New(t) + opts, err := getOpts(WithBindCredential(testCtx, "", "password")) + require.Error(t, err) + assert.Empty(opts.withBindDn) + assert.Empty(opts.withBindPassword) + }) + t.Run("WithBindCredential-missing-password", func(t *testing.T) { + assert := assert.New(t) + opts, err := getOpts(WithBindCredential(testCtx, "dn", "")) + require.Error(t, err) + assert.Empty(opts.withBindDn) + assert.Empty(opts.withBindPassword) + }) + t.Run("WithBindCredential-missing-dn-and-password", func(t *testing.T) { + assert := assert.New(t) + opts, err := getOpts(WithBindCredential(testCtx, "", "")) + require.Error(t, err) + assert.Empty(opts.withBindDn) + }) + t.Run("WithCertificates", func(t *testing.T) { + assert := assert.New(t) + testCert, _ := TestGenerateCA(t, "localhost") + testCert2, _ := TestGenerateCA(t, "127.0.0.1") + + opts, err := getOpts(WithCertificates(testCtx, testCert, testCert2)) + require.NoError(t, err) + testOpts := getDefaultOptions() + encodedCerts, err := EncodeCertificates(testCtx, testCert, testCert2) + require.NoError(t, err) + testOpts.withCertificates = encodedCerts + assert.Equal(opts, testOpts) + }) + t.Run("WithClientCertificate", func(t *testing.T) { + assert := assert.New(t) + testCert, testCertEncoded := TestGenerateCA(t, "localhost") + _, testPrivKey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + derPrivKey, err := x509.MarshalPKCS8PrivateKey(testPrivKey) + require.NoError(t, err) + + opts, err := getOpts(WithClientCertificate(testCtx, derPrivKey, testCert)) + require.NoError(t, err) + testOpts := getDefaultOptions() + testOpts.withClientCertificate = testCertEncoded + testOpts.withClientCertificateKey = derPrivKey + assert.Equal(opts, testOpts) + + // missing privKey + _, err = getOpts(WithClientCertificate(testCtx, nil, testCert)) + require.Error(t, err) + assert.Contains(err.Error(), "missing private key") + + // missing cert + _, err = getOpts(WithClientCertificate(testCtx, derPrivKey, nil)) + require.Error(t, err) + assert.Contains(err.Error(), "missing certificate") + + // bad privKey + _, err = getOpts(WithClientCertificate(testCtx, []byte("not-a-kay"), testCert)) + require.Error(t, err) + assert.Contains(err.Error(), "asn1: structure error") + }) + t.Run("WithLimit", func(t *testing.T) { + opts, err := getOpts(WithLimit(testCtx, 5)) + require.NoError(t, err) + testOpts := getDefaultOptions() + testOpts.withLimit = 5 + assert.Equal(t, opts, testOpts) + }) + t.Run("WithUnauthenticatedUser", func(t *testing.T) { + assert := assert.New(t) + opts, err := getOpts(WithUnauthenticatedUser(testCtx, true)) + require.NoError(t, err) + testOpts := getDefaultOptions() + testOpts.withUnauthenticatedUser = true + assert.Equal(opts, testOpts) + }) + t.Run("WithOrderByCreateTime", func(t *testing.T) { + assert := assert.New(t) + opts, err := getOpts(WithOrderByCreateTime(testCtx, true)) + require.NoError(t, err) + testOpts := getDefaultOptions() + testOpts.withOrderByCreateTime = true + testOpts.ascending = true + assert.Equal(opts, testOpts) + }) + t.Run("WithOperationalState", func(t *testing.T) { + assert := assert.New(t) + opts, err := getOpts(WithOperationalState(testCtx, ActivePublicState)) + require.NoError(t, err) + testOpts := getDefaultOptions() + testOpts.withOperationalState = ActivePublicState + assert.Equal(opts, testOpts) + }) + t.Run("WithDn", func(t *testing.T) { + assert := assert.New(t) + opts, err := getOpts(WithDn(testCtx, "test")) + require.NoError(t, err) + testOpts := getDefaultOptions() + assert.NotEqual(opts, testOpts) + testOpts.withDn = "test" + assert.Equal(opts, testOpts) + }) + t.Run("WithFullName", func(t *testing.T) { + assert := assert.New(t) + opts, err := getOpts(WithFullName(testCtx, "test")) + require.NoError(t, err) + testOpts := getDefaultOptions() + assert.NotEqual(opts, testOpts) + testOpts.withFullName = "test" + assert.Equal(opts, testOpts) + }) + t.Run("WithEmail", func(t *testing.T) { + assert := assert.New(t) + opts, err := getOpts(WithEmail(testCtx, "test")) + require.NoError(t, err) + testOpts := getDefaultOptions() + assert.NotEqual(opts, testOpts) + testOpts.withEmail = "test" + assert.Equal(opts, testOpts) + }) + t.Run("WithMemberOfGroups", func(t *testing.T) { + assert := assert.New(t) + opts, err := getOpts(WithMemberOfGroups(testCtx, "test")) + require.NoError(t, err) + testOpts := getDefaultOptions() + assert.NotEqual(opts, testOpts) + testOpts.withMemberOfGroups = "[\"test\"]" + assert.Equal(opts, testOpts) + }) +} diff --git a/internal/auth/ldap/repository.go b/internal/auth/ldap/repository.go new file mode 100644 index 0000000000..ef46a7b291 --- /dev/null +++ b/internal/auth/ldap/repository.go @@ -0,0 +1,55 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/util" +) + +// RepoFactory is a factory function that returns a repository and any error +type RepoFactory func() (*Repository, error) + +// Repository is the ldap repository +type Repository struct { + reader db.Reader + writer db.Writer + kms kms.GetWrapperer + + // defaultLimit provides a default for limiting the number of results returned from the repo + defaultLimit int +} + +// NewRepository creates a new ldap Repository. Supports the options: WithLimit +// which sets a default limit on results returned by repo operations. +func NewRepository(ctx context.Context, r db.Reader, w db.Writer, kms kms.GetWrapperer, opt ...Option) (*Repository, error) { + const op = "ldap.NewRepository" + if r == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "reader is nil") + } + if w == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "writer is nil") + } + if util.IsNil(kms) { + return nil, errors.New(ctx, errors.InvalidParameter, op, "kms is nil") + } + opts, err := getOpts(opt...) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + if opts.withLimit == 0 { + // zero signals the boundary defaults should be used. + opts.withLimit = db.DefaultLimit + } + return &Repository{ + reader: r, + writer: w, + kms: kms, + defaultLimit: opts.withLimit, + }, nil +} diff --git a/internal/auth/ldap/repository_account.go b/internal/auth/ldap/repository_account.go new file mode 100644 index 0000000000..e61fa122f8 --- /dev/null +++ b/internal/auth/ldap/repository_account.go @@ -0,0 +1,254 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "fmt" + "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" +) + +// CreateAccount inserts an Account, a, into the repository and returns a +// new Account containing its PublicId. a is not changed. a must contain a +// valid LdapMethodId and ScopeId. a must not contain a PublicId. The PublicId +// is generated and assigned by this method. a must contain a valid LoginName. +// a.LoginName must be unique for an a.AuthMethod. +// +// Both a.Name and a.Description are optional. If a.Name is set, it must be +// unique within a.AuthMethodId. +func (r *Repository) CreateAccount(ctx context.Context, a *Account, _ ...Option) (*Account, error) { + const op = "ldap.(Repository).CreateAccount" + switch { + case a == nil: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing account") + case a.Account == nil: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing embedded account") + case a.PublicId != "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "public id must be empty") + default: + if err := a.validate(ctx, op); err != nil { + return nil, err // err already wrapped + } + } + id, err := newAccountId(ctx, a.AuthMethodId, a.LoginName) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + a.PublicId = id + + oplogWrapper, err := r.kms.GetWrapper(ctx, a.ScopeId, kms.KeyPurposeOplog) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"), errors.WithCode(errors.Encrypt)) + } + + md, err := a.oplog(ctx, oplog.OpType_OP_TYPE_CREATE) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to generate account oplog metadata")) + } + var newAccount *Account + _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + newAccount = a.clone() + if err := w.Create(ctx, newAccount, db.WithOplog(oplogWrapper, md)); err != nil { + return err + } + return nil + }, + ) + + if err != nil { + switch { + case errors.IsUniqueError(err): + return nil, errors.New(ctx, errors.NotUnique, op, fmt.Sprintf( + "in auth method %s: name %q already exists or login name %q already exists for auth method %q in scope %s", + a.AuthMethodId, a.Name, a.LoginName, a.AuthMethodId, a.ScopeId)) + default: + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(a.AuthMethodId)) + } + } + return newAccount, nil +} + +// LookupAccount will look up an account in the repository. If the account is not +// found, it will return nil, nil. All options are ignored. +func (r *Repository) LookupAccount(ctx context.Context, withPublicId string, _ ...Option) (*Account, error) { + const op = "ldap.(Repository).LookupAccount" + if withPublicId == "" { + return nil, errors.New(ctx, errors.InvalidPublicId, op, "missing public id") + } + a := AllocAccount() + a.PublicId = withPublicId + if err := r.reader.LookupByPublicId(ctx, a); err != nil { + switch { + case errors.IsNotFoundError(err): + return nil, nil + default: + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for %s", withPublicId))) + } + } + return a, nil +} + +// ListAccounts in an auth method and supports WithLimit option. +func (r *Repository) ListAccounts(ctx context.Context, withAuthMethodId string, opt ...Option) ([]*Account, error) { + const op = "ldap.(Repository).ListAccounts" + if withAuthMethodId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id") + } + opts, err := getOpts(opt...) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + limit := r.defaultLimit + if opts.withLimit != 0 { + // non-zero signals an override of the default limit for the repo. + limit = opts.withLimit + } + var accts []*Account + err = r.reader.SearchWhere(ctx, &accts, "auth_method_id = ?", []any{withAuthMethodId}, db.WithLimit(limit)) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + return accts, nil +} + +// DeleteAccount deletes the account for the provided id from the repository returning a count of the +// number of records deleted. All options are ignored. +func (r *Repository) DeleteAccount(ctx context.Context, withPublicId string, _ ...Option) (int, error) { + const op = "ldap.(Repository).DeleteAccount" + switch { + case withPublicId == "": + return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing public id") + } + ac := AllocAccount() + ac.PublicId = withPublicId + + if err := r.reader.LookupById(ctx, ac); err != nil { + return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("account not found")) + } + + oplogWrapper, err := r.kms.GetWrapper(ctx, ac.ScopeId, kms.KeyPurposeOplog) + if err != nil { + return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("unable to get oplog wrapper")) + } + metadata, err := ac.oplog(ctx, oplog.OpType_OP_TYPE_DELETE) + if err != nil { + return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to generate oplog metadata")) + } + + var rowsDeleted int + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) (err error) { + dAc := ac.clone() + rowsDeleted, err = w.Delete(ctx, dAc, db.WithOplog(oplogWrapper, metadata)) + switch { + case err != nil: + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete ldap account")) + case rowsDeleted > 1: + return errors.New(ctx, errors.MultipleRecords, op, "more than 1 resource would have been deleted") + } + return nil + }, + ) + if err != nil { + return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(withPublicId)) + } + + return rowsDeleted, nil +} + +// UpdateAccount updates the repository entry for a.PublicId with the +// values in a for the fields listed in fieldMaskPaths. It returns a new +// Account containing the updated values and a count of the number of +// records updated. a is not changed. +// +// a must contain a valid PublicId. Only a.Name and a.Description can be +// updated. If a.Name is set to a non-empty string, it must be unique within +// a.AuthMethodId. +// +// An attribute of a will be set to NULL in the database if the attribute +// in a is the zero value and it is included in fieldMaskPaths. +func (r *Repository) UpdateAccount(ctx context.Context, scopeId string, a *Account, version uint32, fieldMaskPaths []string, opt ...Option) (*Account, int, error) { + const op = "ldap.(Repository).UpdateAccount" + switch { + case a == nil: + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing Account") + case scopeId == "": + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing scope id") + case version == 0: + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version") + case a.Account == nil: + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing embedded Account") + case a.PublicId == "": + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidPublicId, op, "missing public id") + } + for _, f := range fieldMaskPaths { + switch { + case strings.EqualFold(NameField, f): + case strings.EqualFold(DescriptionField, f): + default: + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidFieldMask, op, f) + } + } + var dbMask, nullFields []string + dbMask, nullFields = dbw.BuildUpdatePaths( + map[string]any{ + NameField: a.Name, + DescriptionField: a.Description, + }, + fieldMaskPaths, + nil, + ) + if len(dbMask) == 0 && len(nullFields) == 0 { + return nil, db.NoRowsAffected, errors.New(ctx, errors.EmptyFieldMask, op, "missing field mask") + } + + oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeOplog) + if err != nil { + return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), + errors.WithMsg(("unable to get oplog wrapper"))) + } + + metadata, err := a.oplog(ctx, oplog.OpType_OP_TYPE_UPDATE) + if err != nil { + return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to generate oplog metadata")) + } + + var rowsUpdated int + var returnedAccount *Account + _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + returnedAccount = a.clone() + var err error + rowsUpdated, err = w.Update(ctx, returnedAccount, dbMask, nullFields, db.WithOplog(oplogWrapper, metadata), db.WithVersion(&version)) + if err != nil { + return errors.Wrap(ctx, err, op) + } + if rowsUpdated > 1 { + return errors.New(ctx, errors.MultipleRecords, op, "more than 1 resource would have been updated") + } + return nil + }, + ) + if err != nil { + switch { + case errors.IsUniqueError(err): + return nil, db.NoRowsAffected, errors.New(ctx, errors.NotUnique, op, + fmt.Sprintf("name %s already exists: %s", a.Name, a.PublicId)) + default: + return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(a.PublicId)) + } + } + + return returnedAccount, rowsUpdated, nil +} diff --git a/internal/auth/ldap/repository_account_test.go b/internal/auth/ldap/repository_account_test.go new file mode 100644 index 0000000000..2b3908ca39 --- /dev/null +++ b/internal/auth/ldap/repository_account_test.go @@ -0,0 +1,1343 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "fmt" + "sort" + "strings" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/auth/ldap/store" + "github.com/hashicorp/boundary/internal/db" + dbassert "github.com/hashicorp/boundary/internal/db/assert" + "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_CreateAccount(t *testing.T) { + testConn, _ := db.TestSetup(t, "postgres") + testRw := db.New(testConn) + testWrapper := db.TestWrapper(t) + + testKms := kms.TestKms(t, testConn, testWrapper) + iamRepo := iam.TestRepo(t, testConn, testWrapper) + org, _ := iam.TestScopes(t, iamRepo) + + testCtx := context.Background() + databaseWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + testRepo, err := NewRepository(testCtx, testRw, testRw, testKms) + assert.NoError(t, err) + + authMethod := TestAuthMethod(t, testConn, databaseWrapper, org.GetPublicId(), []string{"ldaps://ldap1"}) + + tests := []struct { + name string + repo *Repository + in *Account + opts []Option + want *Account + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "missing-account", + repo: testRepo, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing account: parameter violation: error #100", + }, + { + name: "missing-embedded-account", + in: &Account{}, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing embedded account: parameter violation: error #100", + }, + { + name: "missing-auth-method-id", + in: &Account{ + Account: &store.Account{ + ScopeId: org.PublicId, + }, + }, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing auth method id: parameter violation: error #100", + }, + { + name: "missing-scope-id", + in: &Account{ + Account: &store.Account{ + AuthMethodId: authMethod.PublicId, + }, + }, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing scope id: parameter violation: error #100", + }, + { + name: "invalid-public-id-set", + in: &Account{ + Account: &store.Account{ + AuthMethodId: authMethod.PublicId, + PublicId: "invalid-public-id-set", + LoginName: "invalid-public-id-set", + }, + }, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "public id must be empty: parameter violation: error #100", + }, + { + name: "missing-login-name", + in: &Account{ + Account: &store.Account{ + AuthMethodId: authMethod.PublicId, + ScopeId: org.PublicId, + }, + }, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing login name: parameter violation: error #100", + }, + { + name: "valid-no-options", + repo: testRepo, + in: &Account{ + Account: &store.Account{ + AuthMethodId: authMethod.PublicId, + ScopeId: org.PublicId, + LoginName: "valid-no-options", + }, + }, + want: &Account{ + Account: &store.Account{ + AuthMethodId: authMethod.PublicId, + ScopeId: org.PublicId, + LoginName: "valid-no-options", + }, + }, + }, + { + name: "valid-with-name", + repo: testRepo, + in: &Account{ + Account: &store.Account{ + AuthMethodId: authMethod.PublicId, + ScopeId: org.PublicId, + LoginName: "valid-with-name", + Name: "test-name-repo", + }, + }, + want: &Account{ + Account: &store.Account{ + AuthMethodId: authMethod.PublicId, + ScopeId: org.PublicId, + LoginName: "valid-with-name", + Name: "test-name-repo", + }, + }, + }, + { + name: "valid-with-description", + repo: testRepo, + in: &Account{ + Account: &store.Account{ + AuthMethodId: authMethod.PublicId, + ScopeId: org.PublicId, + LoginName: "valid-with-description", + Description: ("test-description-repo"), + }, + }, + want: &Account{ + Account: &store.Account{ + AuthMethodId: authMethod.PublicId, + ScopeId: org.PublicId, + LoginName: "valid-with-description", + Description: ("test-description-repo"), + }, + }, + }, + { + name: "get-wrapper-err", + repo: func() *Repository { + kms := &mockGetWrapperer{ + getErr: errors.New(testCtx, errors.Encrypt, "test", "get-db-wrapper-err"), + } + r, err := NewRepository(testCtx, testRw, testRw, kms) + require.NoError(t, err) + return r + }(), + in: &Account{ + Account: &store.Account{ + AuthMethodId: authMethod.PublicId, + ScopeId: org.PublicId, + LoginName: "get-wrapper-err", + }, + }, + wantErrMatch: errors.T(errors.Encrypt), + wantErrContains: "get-db-wrapper-err: encryption issue", + }, + { + name: "write-err", + repo: func() *Repository { + conn, mock := db.TestSetupWithMock(t) + mock.ExpectBegin() + mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "version"}).AddRow("1", 1)) // oplog: get ticket + mock.ExpectQuery(`INSERT`).WillReturnError(fmt.Errorf("write-err")) + mock.ExpectRollback() + rw := db.New(conn) + r, err := NewRepository(testCtx, rw, rw, testKms) + require.NoError(t, err) + return r + }(), + in: &Account{ + Account: &store.Account{ + AuthMethodId: authMethod.PublicId, + ScopeId: org.PublicId, + LoginName: "get-wrapper-err", + }, + }, + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "write-err", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + got, err := tc.repo.CreateAccount(testCtx, tc.in, tc.opts...) + if tc.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tc.wantErrMatch, err), "Unexpected error %s", err) + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + require.NotNil(got) + assertPublicId(t, globals.LdapAccountPrefix, got.PublicId) + assert.NotSame(tc.in, got) + assert.NotEmpty(got.CreateTime) + assert.NotEmpty(got.UpdateTime) + assert.Equal(got.CreateTime, got.UpdateTime) + tc.want.CreateTime = got.CreateTime + tc.want.UpdateTime = got.UpdateTime + tc.want.PublicId = got.PublicId + tc.want.Version = 1 + assert.Empty(cmp.Diff(tc.want, got, protocmp.Transform())) + assert.NoError(db.TestVerifyOplog(t, testRw, got.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second))) + }) + } +} + +func TestRepository_CreateAccount_DuplicateFields(t *testing.T) { + testConn, _ := db.TestSetup(t, "postgres") + testRw := db.New(testConn) + testWrapper := db.TestWrapper(t) + + testKms := kms.TestKms(t, testConn, testWrapper) + iamRepo := iam.TestRepo(t, testConn, testWrapper) + testCtx := context.Background() + + t.Run("invalid-duplicate-names", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(testCtx, testRw, testRw, testKms) + assert.NoError(err) + require.NotNil(repo) + + org, _ := iam.TestScopes(t, iamRepo) + databaseWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + + authMethod := TestAuthMethod(t, testConn, databaseWrapper, org.GetPublicId(), []string{"ldaps://ldap1"}) + + in := &Account{ + Account: &store.Account{ + AuthMethodId: authMethod.GetPublicId(), + ScopeId: org.GetPublicId(), + Name: "test-name-repo", + LoginName: "login-name", + }, + } + + got, err := repo.CreateAccount(context.Background(), in) + require.NoError(err) + require.NotNil(got) + assertPublicId(t, globals.LdapAccountPrefix, got.PublicId) + assert.NotSame(in, got) + assert.Equal(in.Name, got.Name) + assert.Equal(in.Description, got.Description) + assert.Equal(got.CreateTime, got.UpdateTime) + + in.PublicId = "" + got2, err := repo.CreateAccount(context.Background(), in) + assert.Truef(errors.Match(errors.T(errors.NotUnique), err), "Unexpected error %s", err) + assert.Nil(got2) + }) + + t.Run("valid-duplicate-names-diff-parents", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(testCtx, testRw, testRw, testKms) + assert.NoError(err) + require.NotNil(repo) + + org, _ := iam.TestScopes(t, iamRepo) + databaseWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + + authMethodA := TestAuthMethod(t, testConn, databaseWrapper, org.GetPublicId(), []string{"ldaps://ldap1"}) + authMethodB := TestAuthMethod(t, testConn, databaseWrapper, org.GetPublicId(), []string{"ldaps://ldap2"}) + in := &Account{ + Account: &store.Account{ + ScopeId: org.PublicId, + Name: "test-name-repo", + LoginName: "login1", + }, + } + in2 := in.clone() + + in.AuthMethodId = authMethodA.GetPublicId() + got, err := repo.CreateAccount(context.Background(), in) + require.NoError(err) + require.NotNil(got) + assertPublicId(t, globals.LdapAccountPrefix, got.PublicId) + assert.NotSame(in, got) + assert.Equal(in.Name, got.Name) + assert.Equal(in.Description, got.Description) + assert.Equal(got.CreateTime, got.UpdateTime) + + in2.AuthMethodId = authMethodB.GetPublicId() + got2, err := repo.CreateAccount(context.Background(), in2) + assert.NoError(err) + require.NotNil(got2) + assertPublicId(t, globals.LdapAccountPrefix, got2.PublicId) + assert.NotSame(in2, got2) + assert.Equal(in2.Name, got2.Name) + assert.Equal(in2.Description, got2.Description) + assert.Equal(got2.CreateTime, got2.UpdateTime) + }) + + t.Run("invalid-duplicate-login-names", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(testCtx, testRw, testRw, testKms) + assert.NoError(err) + require.NotNil(repo) + + org, _ := iam.TestScopes(t, iamRepo) + databaseWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + + authMethod := TestAuthMethod(t, testConn, databaseWrapper, org.GetPublicId(), []string{"ldaps://ldap1"}) + in := &Account{ + Account: &store.Account{ + AuthMethodId: authMethod.GetPublicId(), + ScopeId: org.PublicId, + LoginName: "login-name-1", + }, + } + + got, err := repo.CreateAccount(context.Background(), in) + require.NoError(err) + require.NotNil(got) + assertPublicId(t, globals.LdapAccountPrefix, got.PublicId) + assert.NotSame(in, got) + assert.Equal(in.Name, got.Name) + assert.Equal(in.Description, got.Description) + assert.Equal(got.CreateTime, got.UpdateTime) + + in.PublicId = "" + got2, err := repo.CreateAccount(context.Background(), in) + assert.Truef(errors.Match(errors.T(errors.NotUnique), err), "Unexpected error %s", err) + assert.Nil(got2) + }) + + t.Run("valid-duplicate-login-name-diff-auth-method", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(testCtx, testRw, testRw, testKms) + assert.NoError(err) + require.NotNil(repo) + + org, _ := iam.TestScopes(t, iamRepo) + databaseWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + + authMethodA := TestAuthMethod(t, testConn, databaseWrapper, org.GetPublicId(), []string{"ldaps://ldap1"}) + authMethodB := TestAuthMethod(t, testConn, databaseWrapper, org.GetPublicId(), []string{"ldaps://ldap2"}) + in := &Account{ + Account: &store.Account{ + AuthMethodId: authMethodA.PublicId, + ScopeId: org.PublicId, + LoginName: "login-name-1", + }, + } + in2 := in.clone() + + in.AuthMethodId = authMethodA.GetPublicId() + got, err := repo.CreateAccount(context.Background(), in) + require.NoError(err) + require.NotNil(got) + assertPublicId(t, globals.LdapAccountPrefix, got.PublicId) + assert.NotSame(in, got) + assert.Equal(in.Name, got.Name) + assert.Equal(in.Description, got.Description) + assert.Equal(in.LoginName, got.LoginName) + assert.Equal(got.CreateTime, got.UpdateTime) + + in2.AuthMethodId = authMethodB.GetPublicId() + got2, err := repo.CreateAccount(context.Background(), in2) + assert.NoError(err) + require.NotNil(got2) + assertPublicId(t, globals.LdapAccountPrefix, got2.PublicId) + assert.NotSame(in2, got2) + assert.Equal(in2.Name, got2.Name) + assert.Equal(in2.Description, got2.Description) + assert.Equal(in2.LoginName, got2.LoginName) + assert.Equal(got2.CreateTime, got2.UpdateTime) + }) +} + +func TestRepository_LookupAccount(t *testing.T) { + testConn, _ := db.TestSetup(t, "postgres") + testRw := db.New(testConn) + testWrapper := db.TestWrapper(t) + + testKms := kms.TestKms(t, testConn, testWrapper) + iamRepo := iam.TestRepo(t, testConn, testWrapper) + org, _ := iam.TestScopes(t, iamRepo) + + testCtx := context.Background() + databaseWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + testRepo, err := NewRepository(testCtx, testRw, testRw, testKms) + assert.NoError(t, err) + + authMethod := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}) + account := TestAccount(t, testConn, authMethod, "test-login-name") + + newAcctId, err := newAccountId(testCtx, authMethod.PublicId, "test-not-matching") + require.NoError(t, err) + tests := []struct { + name string + repo *Repository + publicId string + want *Account + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "missing-public-id", + repo: testRepo, + wantErrMatch: errors.T(errors.InvalidPublicId), + wantErrContains: "missing public id: parameter violation: error #102", + }, + { + name: "not-found", + repo: testRepo, + publicId: newAcctId, + }, + { + name: "read-err", + repo: func() *Repository { + conn, mock := db.TestSetupWithMock(t) + mock.ExpectQuery(`SELECT`).WillReturnError(fmt.Errorf("read-err")) + rw := db.New(conn) + r, err := NewRepository(testCtx, rw, rw, testKms) + require.NoError(t, err) + return r + }(), + publicId: newAcctId, + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "read-err", + }, + { + name: "with-existing-account-id", + repo: testRepo, + publicId: account.GetPublicId(), + want: account, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + got, err := tc.repo.LookupAccount(testCtx, tc.publicId) + if tc.wantErrMatch != nil { + assert.Truef(errors.Match(tc.wantErrMatch, err), "Unexpected error %s", err) + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.EqualValues(tc.want, got) + }) + } +} + +func TestRepository_DeleteAccount(t *testing.T) { + testConn, _ := db.TestSetup(t, "postgres") + testRw := db.New(testConn) + testWrapper := db.TestWrapper(t) + + testCtx := context.Background() + testKms := kms.TestKms(t, testConn, testWrapper) + iamRepo := iam.TestRepo(t, testConn, testWrapper) + org, _ := iam.TestScopes(t, iamRepo) + + databaseWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + authMethod := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}) + account := TestAccount(t, testConn, authMethod, "create-success") + newAcctId, err := newAccountId(testCtx, authMethod.PublicId, "not-matching") + require.NoError(t, err) + + testRepo, err := NewRepository(testCtx, testRw, testRw, testKms) + assert.NoError(t, err) + require.NotNil(t, testRepo) + + tests := []struct { + name string + repo *Repository + publicId string + want int + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "missing-public-id", + repo: testRepo, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing public id: parameter violation: error #100", + }, + { + name: "not-found", + repo: testRepo, + publicId: newAcctId, + wantErrMatch: errors.T(errors.RecordNotFound), + wantErrContains: "account not found", + }, + { + name: "get-wrapper-err", + repo: func() *Repository { + kms := &mockGetWrapperer{ + getErr: errors.New(testCtx, errors.Encrypt, "test", "get-db-wrapper-err"), + } + r, err := NewRepository(testCtx, testRw, testRw, kms) + require.NoError(t, err) + return r + }(), + publicId: account.GetPublicId(), + wantErrMatch: errors.T(errors.Encrypt), + wantErrContains: "unable to get oplog wrapper", + }, + { + name: "oplog-metadata-err", + repo: func() *Repository { + conn, mock := db.TestSetupWithMock(t) + mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "scope_id"}).AddRow("1", "global")) // get account without auth method id + rw := db.New(conn) + r, err := NewRepository(testCtx, rw, rw, testKms) + require.NoError(t, err) + return r + }(), + publicId: account.GetPublicId(), + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "ldap.(Account).oplog: missing auth method id", + }, + { + name: "write-err", + repo: func() *Repository { + conn, mock := db.TestSetupWithMock(t) + mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "scope_id", "auth_method_id"}).AddRow("1", "global", "1")) // get account + mock.ExpectBegin() + mock.ExpectQuery(`SELECT`).WillReturnError(fmt.Errorf("write-err")) // oplog: get ticket + mock.ExpectRollback() + rw := db.New(conn) + r, err := NewRepository(testCtx, rw, rw, testKms) + require.NoError(t, err) + return r + }(), + publicId: account.GetPublicId(), + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "write-err", + }, + { + name: "too-many-rows-affected", + repo: func() *Repository { + conn, mock := db.TestSetupWithMock(t) + mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "scope_id", "auth_method_id"}).AddRow("1", "global", "1")) // get account + mock.ExpectBegin() + mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "version"}).AddRow("1", 1)) // oplog: get ticket + mock.ExpectExec(`DELETE`).WillReturnResult(sqlmock.NewResult(1, 2)) + mock.ExpectQuery(`INSERT`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("1")) // oplog: entry + mock.ExpectQuery(`INSERT`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("1")) // oplog: metadata + mock.ExpectExec(`UPDATE`).WillReturnResult(sqlmock.NewResult(0, 1)) // oplog: update ticket version + mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "version"}).AddRow("1", 1)) // oplog: get ticket + mock.ExpectRollback() + rw := db.New(conn) + r, err := NewRepository(testCtx, rw, rw, testKms) + require.NoError(t, err) + return r + }(), + publicId: account.GetPublicId(), + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "more than 1 resource would have been deleted", + }, + { + name: "delete-err", + repo: func() *Repository { + conn, mock := db.TestSetupWithMock(t) + mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "scope_id", "auth_method_id"}).AddRow("1", "global", "1")) // get account + mock.ExpectBegin() + mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "version"}).AddRow("1", 1)) // oplog: get ticket + mock.ExpectExec(`DELETE`).WillReturnError(fmt.Errorf("delete-err")) + mock.ExpectRollback() + rw := db.New(conn) + r, err := NewRepository(testCtx, rw, rw, testKms) + require.NoError(t, err) + return r + }(), + publicId: account.GetPublicId(), + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "delete-err", + }, + { + name: "success", + repo: testRepo, + publicId: account.GetPublicId(), + want: 1, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := tc.repo.DeleteAccount(context.Background(), tc.publicId) + if tc.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tc.wantErrMatch, err), "Unexpected error %s", err) + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.EqualValues(tc.want, got) + }) + } +} + +func TestRepository_ListAccounts(t *testing.T) { + testConn, _ := db.TestSetup(t, "postgres") + testRw := db.New(testConn) + testWrapper := db.TestWrapper(t) + + testCtx := context.Background() + testKms := kms.TestKms(t, testConn, testWrapper) + iamRepo := iam.TestRepo(t, testConn, testWrapper) + org, _ := iam.TestScopes(t, iamRepo) + + databaseWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + testRepo, err := NewRepository(testCtx, testRw, testRw, testKms) + assert.NoError(t, err) + + authMethod1 := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}) + authMethod2 := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap2"}) + authMethod3 := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap2"}) + accounts1 := []*Account{ + TestAccount(t, testConn, authMethod1, "create-success"), + TestAccount(t, testConn, authMethod1, "create-success2"), + TestAccount(t, testConn, authMethod1, "create-success3"), + } + accounts2 := []*Account{ + TestAccount(t, testConn, authMethod2, "create-success"), + TestAccount(t, testConn, authMethod2, "create-success2"), + TestAccount(t, testConn, authMethod2, "create-success3"), + } + _ = accounts2 + + tests := []struct { + name string + repo *Repository + publicId string + opts []Option + want []*Account + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "missing-auth-method-id", + repo: testRepo, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing auth method id: parameter violation: error #100", + }, + { + name: "with-no-account-ids", + repo: testRepo, + publicId: authMethod3.GetPublicId(), + want: []*Account{}, + }, + { + name: "with-first-auth-method-id", + repo: testRepo, + publicId: authMethod1.GetPublicId(), + want: accounts1, + }, + { + name: "read-err", + repo: func() *Repository { + conn, mock := db.TestSetupWithMock(t) + mock.ExpectQuery(`SELECT`).WillReturnError(fmt.Errorf("read-err")) + rw := db.New(conn) + r, err := NewRepository(testCtx, rw, rw, testKms) + require.NoError(t, err) + return r + }(), + publicId: authMethod1.GetPublicId(), + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "read-err", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := tc.repo.ListAccounts(testCtx, tc.publicId, tc.opts...) + if tc.wantErrMatch != nil { + assert.Truef(errors.Match(tc.wantErrMatch, err), "Unexpected error %s", err) + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + + sort.Slice(got, func(i, j int) bool { + return strings.Compare(got[i].LoginName, got[j].LoginName) < 0 + }) + assert.EqualValues(tc.want, got) + }) + } +} + +func TestRepository_ListAccounts_Limits(t *testing.T) { + testConn, _ := db.TestSetup(t, "postgres") + testRw := db.New(testConn) + testWrapper := db.TestWrapper(t) + + testCtx := context.Background() + testKms := kms.TestKms(t, testConn, testWrapper) + iamRepo := iam.TestRepo(t, testConn, testWrapper) + org, _ := iam.TestScopes(t, iamRepo) + + databaseWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + am := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}) + + accountCount := 10 + for i := 0; i < accountCount; i++ { + TestAccount(t, testConn, am, fmt.Sprintf("create-success-%d", i)) + } + + tests := []struct { + name string + repoOpts []Option + listOpts []Option + wantLen int + }{ + { + name: "with-no-limits", + wantLen: accountCount, + }, + { + name: "with-repo-limit", + repoOpts: []Option{WithLimit(testCtx, 3)}, + wantLen: 3, + }, + { + name: "with-negative-repo-limit", + repoOpts: []Option{WithLimit(testCtx, -1)}, + wantLen: accountCount, + }, + { + name: "with-list-limit", + listOpts: []Option{WithLimit(testCtx, 3)}, + wantLen: 3, + }, + { + name: "with-negative-list-limit", + listOpts: []Option{WithLimit(testCtx, -1)}, + wantLen: accountCount, + }, + { + name: "with-repo-smaller-than-list-limit", + repoOpts: []Option{WithLimit(testCtx, 2)}, + listOpts: []Option{WithLimit(testCtx, 6)}, + wantLen: 6, + }, + { + name: "with-repo-larger-than-list-limit", + repoOpts: []Option{WithLimit(testCtx, 6)}, + listOpts: []Option{WithLimit(testCtx, 2)}, + wantLen: 2, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(testCtx, testRw, testRw, testKms, tc.repoOpts...) + assert.NoError(err) + require.NotNil(repo) + got, err := repo.ListAccounts(context.Background(), am.GetPublicId(), tc.listOpts...) + require.NoError(err) + assert.Len(got, tc.wantLen) + }) + } +} + +func TestRepository_UpdateAccount(t *testing.T) { + testCtx := context.Background() + testConn, _ := db.TestSetup(t, "postgres") + testRw := db.New(testConn) + testWrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, testConn, testWrapper) + iamRepo := iam.TestRepo(t, testConn, testWrapper) + org, _ := iam.TestScopes(t, iamRepo) + + databaseWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + am := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}) + + testRepo, err := NewRepository(testCtx, testRw, testRw, testKms) + assert.NoError(t, err) + require.NotNil(t, testRepo) + + changeName := func(s string) func(*Account) *Account { + return func(a *Account) *Account { + a.Name = s + return a + } + } + + changeDescription := func(s string) func(*Account) *Account { + return func(a *Account) *Account { + a.Description = s + return a + } + } + + makeNil := func() func(*Account) *Account { + return func(a *Account) *Account { + return nil + } + } + + makeEmbeddedNil := func() func(*Account) *Account { + return func(a *Account) *Account { + return &Account{} + } + } + + deletePublicId := func() func(*Account) *Account { + return func(a *Account) *Account { + a.PublicId = "" + return a + } + } + + nonExistentPublicId := func() func(*Account) *Account { + return func(a *Account) *Account { + a.PublicId = "abcd_OOOOOOOOOO" + return a + } + } + + combine := func(fns ...func(a *Account) *Account) func(*Account) *Account { + return func(a *Account) *Account { + for _, fn := range fns { + a = fn(a) + } + return a + } + } + + tests := []struct { + name string + repo *Repository + scopeId string + version uint32 + orig *Account + chgFn func(*Account) *Account + masks []string + want *Account + wantCount int + wantIsErr errors.Code + wantErrContains string + }{ + { + name: "nil-account", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{}, + }, + chgFn: makeNil(), + masks: []string{NameField, DescriptionField}, + wantIsErr: errors.InvalidParameter, + wantErrContains: "missing Account: parameter violation: error #100", + }, + { + name: "nil-embedded-account", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{}, + }, + chgFn: makeEmbeddedNil(), + masks: []string{NameField, DescriptionField}, + wantIsErr: errors.InvalidParameter, + wantErrContains: "missing embedded Account: parameter violation: error #100", + }, + { + name: "no-scope-id", + repo: testRepo, + version: 1, + orig: &Account{ + Account: &store.Account{ + Name: "no-scope-id-test-name-repo", + }, + }, + chgFn: changeName("no-scope-id-test-update-name-repo"), + masks: []string{NameField}, + wantIsErr: errors.InvalidParameter, + wantErrContains: "missing scope id: parameter violation: error #100", + }, + { + name: "missing-version", + repo: testRepo, + scopeId: org.GetPublicId(), + orig: &Account{ + Account: &store.Account{ + Name: "missing-version-test-name-repo", + }, + }, + chgFn: changeName("test-update-name-repo"), + masks: []string{NameField}, + wantIsErr: errors.InvalidParameter, + wantErrContains: "missing version: parameter violation: error #100", + }, + { + name: "no-public-id", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{}, + }, + chgFn: deletePublicId(), + masks: []string{NameField, DescriptionField}, + wantIsErr: errors.InvalidPublicId, + wantErrContains: "missing public id: parameter violation: error #102", + }, + { + name: "updating-non-existent-account", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{ + Name: "updating-non-existent-Account-test-name-repo", + }, + }, + chgFn: combine(nonExistentPublicId(), changeName("updating-non-existent-Account-test-update-name-repo")), + masks: []string{NameField}, + wantIsErr: errors.RecordNotFound, + wantErrContains: "record not found", + }, + { + name: "empty-field-mask", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{ + Name: "empty-field-mask-test-name-repo", + }, + }, + chgFn: changeName("empty-field-mask-test-update-name-repo"), + wantIsErr: errors.EmptyFieldMask, + wantErrContains: "missing field mask: parameter violation: error #104", + }, + { + name: "read-only-fields-in-field-mask", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{ + Name: "read-only-fields-in-field-mask-test-name-repo", + }, + }, + chgFn: changeName("read-only-fields-in-field-mask-test-update-name-repo"), + masks: []string{"PublicId", "CreateTime", "UpdateTime", "AuthMethodId"}, + wantIsErr: errors.InvalidFieldMask, + wantErrContains: "PublicId: parameter violation: error #103", + }, + { + name: "unknown-field-in-field-mask", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{ + Name: "unknown-field-in-field-mask-test-name-repo", + }, + }, + chgFn: changeName("unknown-field-in-field-mask-test-update-name-repo"), + masks: []string{"Bilbo"}, + wantIsErr: errors.InvalidFieldMask, + wantErrContains: "Bilbo: parameter violation: error #103", + }, + { + name: "change-name", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{ + Name: "change-name-test-name-repo", + }, + }, + chgFn: changeName("change-name-test-update-name-repo"), + masks: []string{NameField}, + want: &Account{ + Account: &store.Account{ + Name: "change-name-test-update-name-repo", + }, + }, + wantCount: 1, + }, + { + name: "change-description", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{ + Description: "test-description-repo", + }, + }, + chgFn: changeDescription("test-update-description-repo"), + masks: []string{DescriptionField}, + want: &Account{ + Account: &store.Account{ + Description: "test-update-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "change-name-and-description", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{ + Name: "change-name-and-description-test-name-repo", + Description: "test-description-repo", + }, + }, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("change-name-and-description-test-update-name-repo")), + masks: []string{NameField, DescriptionField}, + want: &Account{ + Account: &store.Account{ + Name: "change-name-and-description-test-update-name-repo", + Description: "test-update-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "delete-name", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{ + Name: "delete-name-test-name-repo", + Description: "test-description-repo", + }, + }, + masks: []string{NameField}, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("")), + want: &Account{ + Account: &store.Account{ + Description: "test-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "delete-description", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{ + Name: "delete-description-test-name-repo", + Description: "test-description-repo", + }, + }, + masks: []string{DescriptionField}, + chgFn: combine(changeDescription(""), changeName("test-update-name-repo")), + want: &Account{ + Account: &store.Account{ + Name: "delete-description-test-name-repo", + }, + }, + wantCount: 1, + }, + { + name: "do-not-delete-name", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{ + Name: "do-not-delete-name-test-name-repo", + Description: "test-description-repo", + }, + }, + masks: []string{DescriptionField}, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("")), + want: &Account{ + Account: &store.Account{ + Name: "do-not-delete-name-test-name-repo", + Description: "test-update-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "do-not-delete-description", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{ + Name: "do-not-delete-description-test-name-repo", + Description: "test-description-repo", + }, + }, + masks: []string{NameField}, + chgFn: combine(changeDescription(""), changeName("do-not-delete-description-test-update-name-repo")), + want: &Account{ + Account: &store.Account{ + Name: "do-not-delete-description-test-update-name-repo", + Description: "test-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "get-wrapper-err", + repo: func() *Repository { + kms := &mockGetWrapperer{ + getErr: errors.New(testCtx, errors.Encrypt, "test", "get-db-wrapper-err"), + } + r, err := NewRepository(testCtx, testRw, testRw, kms) + require.NoError(t, err) + return r + }(), + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{ + Name: "get-wrapper-err", + Description: "test-description-repo", + }, + }, + masks: []string{NameField}, + chgFn: combine(changeDescription(""), changeName("get-wrapper-err-test-update-name-repo")), + wantIsErr: errors.Encrypt, + wantErrContains: "get-db-wrapper-err: encryption issue", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + orig := TestAccount(t, testConn, am, tc.name, WithName(testCtx, tc.orig.GetName()), WithDescription(testCtx, tc.orig.GetDescription())) + + tc.orig.AuthMethodId = am.PublicId + if tc.chgFn != nil { + orig = tc.chgFn(orig) + } + got, gotCount, err := tc.repo.UpdateAccount(context.Background(), tc.scopeId, orig, tc.version, tc.masks) + if tc.wantIsErr != 0 { + assert.Truef(errors.Match(errors.T(tc.wantIsErr), err), "want err: %q got: %q", tc.wantIsErr, err) + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + assert.Equal(tc.wantCount, gotCount, "row count") + assert.Nil(got) + return + } + assert.NoError(err) + assert.Empty(tc.orig.PublicId) + if tc.wantCount == 0 { + assert.Equal(tc.wantCount, gotCount, "row count") + assert.Nil(got) + return + } + require.NotNil(got) + assertPublicId(t, globals.LdapAccountPrefix, got.PublicId) + assert.Equal(tc.wantCount, gotCount, "row count") + assert.NotSame(tc.orig, got) + assert.Equal(tc.orig.AuthMethodId, got.AuthMethodId) + underlyingDB, err := testConn.SqlDB(testCtx) + require.NoError(err) + dbassert := dbassert.New(t, underlyingDB) + if tc.want.Name == "" { + dbassert.IsNull(got, "name") + return + } + assert.Equal(tc.want.Name, got.Name) + if tc.want.Description == "" { + dbassert.IsNull(got, "description") + return + } + assert.Equal(tc.want.Description, got.Description) + if tc.wantCount > 0 { + assert.NoError(db.TestVerifyOplog(t, testRw, got.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) + } + }) + } +} + +func TestRepository_UpdateAccount_DupeNames(t *testing.T) { + testCtx := context.Background() + testConn, _ := db.TestSetup(t, "postgres") + testRw := db.New(testConn) + testWrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, testConn, testWrapper) + iamRepo := iam.TestRepo(t, testConn, testWrapper) + + t.Run("invalid-duplicate-names", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(testCtx, testRw, testRw, testKms) + assert.NoError(err) + require.NotNil(repo) + + name := "test-dup-name" + org, _ := iam.TestScopes(t, iamRepo) + databaseWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + + am := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}) + aa := TestAccount(t, testConn, am, "create-success1") + ab := TestAccount(t, testConn, am, "create-success2") + + aa.Name = name + got1, gotCount1, err := repo.UpdateAccount(context.Background(), org.GetPublicId(), aa, 1, []string{NameField}) + assert.NoError(err) + require.NotNil(got1) + assert.Equal(name, got1.Name) + assert.Equal(1, gotCount1, "row count") + assert.NoError(db.TestVerifyOplog(t, testRw, aa.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) + + ab.Name = name + got2, gotCount2, err := repo.UpdateAccount(context.Background(), org.GetPublicId(), ab, 1, []string{NameField}) + assert.Truef(errors.Match(errors.T(errors.NotUnique), err), "Unexpected error %s", err) + assert.Nil(got2) + assert.Equal(db.NoRowsAffected, gotCount2, "row count") + err = db.TestVerifyOplog(t, testRw, ab.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) + assert.Error(err) + assert.True(errors.IsNotFoundError(err)) + }) + + t.Run("valid-duplicate-names-diff-AuthMethods", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(testCtx, testRw, testRw, testKms) + assert.NoError(err) + require.NotNil(repo) + + org, _ := iam.TestScopes(t, iamRepo) + databaseWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + + ama := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}) + aa := TestAccount(t, testConn, ama, "create-success1", WithName(testCtx, "test-name-aa")) + + amb := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap2"}) + ab := TestAccount(t, testConn, amb, "create-success2", WithName(testCtx, "test-name-ab")) + + ab.Name = aa.Name + got3, gotCount3, err := repo.UpdateAccount(context.Background(), org.GetPublicId(), ab, 1, []string{NameField}) + assert.NoError(err) + require.NotNil(got3) + assert.NotSame(ab, got3) + assert.Equal(aa.Name, got3.Name) + assert.Equal(1, gotCount3, "row count") + assert.NoError(db.TestVerifyOplog(t, testRw, ab.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) + }) + + t.Run("change-auth-method-id", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(testCtx, testRw, testRw, testKms) + assert.NoError(err) + require.NotNil(repo) + + org, _ := iam.TestScopes(t, iamRepo) + databaseWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + + ama := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}) + aa := TestAccount(t, testConn, ama, "create-success1") + + amb := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap2"}) + ab := TestAccount(t, testConn, amb, "create-success2") + + assert.NotEqual(aa.AuthMethodId, ab.AuthMethodId) + orig := aa.clone() + + aa.AuthMethodId = ab.AuthMethodId + assert.Equal(aa.AuthMethodId, ab.AuthMethodId) + + got1, gotCount1, err := repo.UpdateAccount(context.Background(), org.GetPublicId(), aa, 1, []string{NameField}) + + assert.NoError(err) + require.NotNil(got1) + assert.Equal(orig.AuthMethodId, got1.AuthMethodId) + assert.Equal(1, gotCount1, "row count") + assert.NoError(db.TestVerifyOplog(t, testRw, aa.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) + }) +} + +func assertPublicId(t *testing.T, prefix, actual string) { + t.Helper() + assert.NotEmpty(t, actual) + parts := strings.Split(actual, "_") + assert.Equalf(t, 2, len(parts), "want one '_' in PublicId, got multiple in %q", actual) + assert.Equalf(t, prefix, parts[0], "PublicId want prefix: %q, got: %q in %q", prefix, parts[0], actual) +} diff --git a/internal/auth/ldap/repository_auth_method_create.go b/internal/auth/ldap/repository_auth_method_create.go new file mode 100644 index 0000000000..0b6aed9b4c --- /dev/null +++ b/internal/auth/ldap/repository_auth_method_create.go @@ -0,0 +1,171 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "fmt" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" +) + +// CreateAuthMethod creates am (*AuthMethod) in the repo along with its +// associated embedded optional value objects (urls, certs, client certs, bind +// creds, user search conf and group search conf) and returns the newly created +// AuthMethod (with its PublicId set) +// +// The AuthMethod's public id and version must be empty (zero values). +// +// All options are ignored. +func (r *Repository) CreateAuthMethod(ctx context.Context, am *AuthMethod, _ ...Option) (*AuthMethod, error) { + const op = "ldap.(Repository).CreateAuthMethod" + switch { + case am == nil: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method") + case am.PublicId != "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "public id must be empty") + case am.Version != 0: + return nil, errors.New(ctx, errors.InvalidParameter, op, "version must be empty") + case am.ScopeId == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope id") + case !validState(am.OperationalState): + return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("invalid state: %q", am.OperationalState)) + case len(am.Urls) == 0: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing urls (there must be at least one)") + } + + var err error + am.PublicId, err = newAuthMethodId(ctx) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + cv, err := am.convertValueObjects(ctx) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + dbWrapper, err := r.kms.GetWrapper(context.Background(), am.ScopeId, kms.KeyPurposeDatabase) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper")) + } + + if cv.BindCredential != nil { + bc, ok := cv.BindCredential.(*BindCredential) + if !ok { + return nil, errors.New(ctx, errors.Internal, op, fmt.Sprintf("invalid type (%T) is not a bind credential", cv.BindCredential)) + } + if err := bc.encrypt(ctx, dbWrapper); err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("failed to encrypt bind credential")) + } + } + if cv.ClientCertificate != nil { + cc, ok := cv.ClientCertificate.(*ClientCertificate) + if !ok { + return nil, errors.New(ctx, errors.Internal, op, fmt.Sprintf("invalid type (%T) is not a client certificate", cv.ClientCertificate)) + } + if err := cc.encrypt(ctx, dbWrapper); err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("failed to encrypt client certificate")) + } + } + + oplogWrapper, err := r.kms.GetWrapper(ctx, am.ScopeId, kms.KeyPurposeOplog) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) + } + + var returnedAuthMethod *AuthMethod + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(r db.Reader, w db.Writer) error { + var () + msgs := make([]*oplog.Message, 0, 4) + ticket, err := w.GetTicket(ctx, am) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket")) + } + + returnedAuthMethod = am.clone() + var amOplogMsg oplog.Message + if err := w.Create(ctx, returnedAuthMethod, db.NewOplogMsg(&amOplogMsg)); err != nil { + return err + } + msgs = append(msgs, &amOplogMsg) + + urlOplogMsgs := make([]*oplog.Message, 0, len(cv.Urls)) + if err := w.CreateItems(ctx, cv.Urls, db.NewOplogMsgs(&urlOplogMsgs)); err != nil { + return err + } + msgs = append(msgs, urlOplogMsgs...) + + if len(cv.Certs) > 0 { + certOplogMsgs := make([]*oplog.Message, 0, len(cv.Certs)) + if err := w.CreateItems(ctx, cv.Certs, db.NewOplogMsgs(&certOplogMsgs)); err != nil { + return err + } + msgs = append(msgs, certOplogMsgs...) + } + if cv.UserEntrySearchConf != nil { + var ucOplogMsg oplog.Message + if err := w.Create(ctx, cv.UserEntrySearchConf, db.NewOplogMsg(&ucOplogMsg)); err != nil { + return err + } + msgs = append(msgs, &ucOplogMsg) + } + if cv.GroupEntrySearchConf != nil { + var gcOplogMsg oplog.Message + if err := w.Create(ctx, cv.GroupEntrySearchConf, db.NewOplogMsg(&gcOplogMsg)); err != nil { + return err + } + msgs = append(msgs, &gcOplogMsg) + } + if cv.ClientCertificate != nil { + var ccOplogMsg oplog.Message + if err := w.Create(ctx, cv.ClientCertificate, db.NewOplogMsg(&ccOplogMsg)); err != nil { + return err + } + msgs = append(msgs, &ccOplogMsg) + } + if cv.BindCredential != nil { + var bcOplogMsg oplog.Message + if err := w.Create(ctx, cv.BindCredential, db.NewOplogMsg(&bcOplogMsg)); err != nil { + return err + } + msgs = append(msgs, &bcOplogMsg) + } + if len(cv.AccountAttributeMaps) > 0 { + attrMapsOplogMsgs := make([]*oplog.Message, 0, len(cv.AccountAttributeMaps)) + if err := w.CreateItems(ctx, cv.AccountAttributeMaps, db.NewOplogMsgs(&attrMapsOplogMsgs)); err != nil { + return err + } + msgs = append(msgs, attrMapsOplogMsgs...) + } + md, err := am.oplog(ctx, oplog.OpType_OP_TYPE_CREATE) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to generate oplog metadata")) + } + if err := w.WriteOplogEntryWith(ctx, oplogWrapper, ticket, md, msgs); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog")) + } + return nil + }, + ) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + found, err := r.getAuthMethods(ctx, am.GetPublicId(), nil) + switch { + case err != nil: + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("failed to lookup created auth method %q", am.GetPublicId())) + case len(found) != 1: + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("found %d auth methods with public id of %q and expected 1", len(found), am.GetPublicId())) + default: + return found[0], nil + } +} diff --git a/internal/auth/ldap/repository_auth_method_create_test.go b/internal/auth/ldap/repository_auth_method_create_test.go new file mode 100644 index 0000000000..05ce8327f7 --- /dev/null +++ b/internal/auth/ldap/repository_auth_method_create_test.go @@ -0,0 +1,272 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "crypto/ed25519" + "crypto/rand" + "crypto/x509" + "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" + wrapping "github.com/hashicorp/go-kms-wrapping/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestRepository_CreateAuthMethod(t *testing.T) { + testConn, _ := db.TestSetup(t, "postgres") + testRw := db.New(testConn) + testWrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, testConn, testWrapper) + testCtx := context.Background() + org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper)) + testCert, _ := TestGenerateCA(t, "localhost") + testCert2, _ := TestGenerateCA(t, "localhost") + _, testPrivKey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + derPrivKey, err := x509.MarshalPKCS8PrivateKey(testPrivKey) + require.NoError(t, err) + + testAm, err := NewAuthMethod( + testCtx, + org.PublicId, + WithUrls(testCtx, TestConvertToUrls(t, "ldaps://ldap1", "ldap://ldap2")...), + WithName(testCtx, "test-name"), + WithDescription(testCtx, "test-description"), + WithStartTLS(testCtx), + WithInsecureTLS(testCtx), + WithDiscoverDn(testCtx), + WithAnonGroupSearch(testCtx), + WithUpnDomain(testCtx, "alice.com"), + WithUserDn(testCtx, "user-dn"), + WithUserAttr(testCtx, "user-attr"), + WithUserFilter(testCtx, "user-filter"), + WithEnableGroups(testCtx), + WithUseTokenGroups(testCtx), + WithGroupDn(testCtx, "group-dn"), + WithGroupAttr(testCtx, "group-attr"), + WithGroupFilter(testCtx, "group-filter"), + WithBindCredential(testCtx, "bind-dn", "bind-password"), + WithCertificates(testCtx, testCert, testCert2), + WithClientCertificate(testCtx, derPrivKey, testCert), // not a client cert but good enough for this test. + WithAccountAttributeMap(testCtx, map[string]AccountToAttribute{ + "mail": ToEmailAttribute, + }), + ) + require.NoError(t, err) + + tests := []struct { + name string + kms kms.GetWrapperer + setup func(*testing.T) *AuthMethod + opt []Option + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "valid", + kms: testKms, + setup: func(t *testing.T) *AuthMethod { + return testAm.clone() + }, + }, + { + name: "bind-cred-encrypt-err", + kms: &mockGetWrapperer{ + returnDbWrapper: &kms.MockWrapper{ + EncryptErr: errors.New(testCtx, errors.Encrypt, "test", "bind-cred-encrypt-err"), + }, + }, + setup: func(t *testing.T) *AuthMethod { + return testAm.clone() + }, + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "bind-cred-encrypt-err", + }, + { + name: "get-db-wrapper-err", + kms: &mockGetWrapperer{ + getErr: errors.New(testCtx, errors.Encrypt, "test", "get-db-wrapper-err"), + }, + setup: func(t *testing.T) *AuthMethod { + return testAm.clone() + }, + wantErrMatch: errors.T(errors.Encrypt), + wantErrContains: "unable to get database wrapper", + }, + { + name: "get-oplog-wrapper-err", + kms: &mockGetWrapperer{ + getErr: errors.New(testCtx, errors.Encrypt, "test", "get-db-wrapper-err"), + returnDbWrapper: testWrapper, + }, + setup: func(t *testing.T) *AuthMethod { + return testAm.clone() + }, + wantErrMatch: errors.T(errors.Encrypt), + wantErrContains: "unable to get oplog wrapper", + }, + { + name: "bad-state", + kms: testKms, + setup: func(t *testing.T) *AuthMethod { + am, err := NewAuthMethod(testCtx, org.PublicId, WithUrls(testCtx, TestConvertToUrls(t, "ldaps://ldap1")...)) + require.NoError(t, err) + am.OperationalState = "not-a-valid-state" + return am + }, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "invalid state", + }, + { + name: "missing-auth-method", + kms: testKms, + setup: func(t *testing.T) *AuthMethod { + return nil + }, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing auth method", + }, + { + name: "missing-scope", + kms: testKms, + setup: func(t *testing.T) *AuthMethod { + am, err := NewAuthMethod(testCtx, org.PublicId, WithUrls(testCtx, TestConvertToUrls(t, "ldaps://ldap1")...)) + require.NoError(t, err) + am.ScopeId = "" + return am + }, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing scope id", + }, + { + name: "convert-err", + kms: testKms, + setup: func(t *testing.T) *AuthMethod { + am, err := NewAuthMethod(testCtx, org.PublicId, WithUrls(testCtx, TestConvertToUrls(t, "ldaps://ldap1")...)) + require.NoError(t, err) + am.BindDn = "bind-dn" + return am + }, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing password", + }, + { + name: "missing-urls", + kms: testKms, + setup: func(t *testing.T) *AuthMethod { + am, err := NewAuthMethod(testCtx, org.PublicId, WithUrls(testCtx, TestConvertToUrls(t, "ldaps://ldap1")...)) + require.NoError(t, err) + am.Urls = nil + return am + }, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing urls (there must be at least one)", + }, + { + name: "bad-public-id", + kms: testKms, + setup: func(t *testing.T) *AuthMethod { + id, err := newAuthMethodId(testCtx) + require.NoError(t, err) + am := AllocAuthMethod() + am.PublicId = id + return &am + }, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "public id must be empty", + }, + { + name: "bad-version", + kms: testKms, + setup: func(t *testing.T) *AuthMethod { + am := AllocAuthMethod() + am.Version = 22 + return &am + }, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "version must be empty", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(testCtx, testRw, testRw, tc.kms) + assert.NoError(err) + require.NotNil(repo) + am := tc.setup(t) + got, err := repo.CreateAuthMethod(testCtx, am, tc.opt...) + if tc.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tc.wantErrMatch, err), "want err code: %q got: %q", tc.wantErrMatch, err) + assert.Nil(got) + + if am != nil { + err := db.TestVerifyOplog(t, testRw, am.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second)) + require.Errorf(err, "should not have found oplog entry for %s", am.PublicId) + } + + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + am.PublicId = got.PublicId + am.CreateTime = got.CreateTime + am.UpdateTime = got.UpdateTime + am.Version = got.Version + am.BindPasswordHmac = got.BindPasswordHmac + am.ClientCertificateKeyHmac = got.ClientCertificateKeyHmac + TestSortAuthMethods(t, []*AuthMethod{am, got}) + assert.Truef(proto.Equal(am.AuthMethod, got.AuthMethod), "got %+v expected %+v", got.AuthMethod, am.AuthMethod) + + err = db.TestVerifyOplog(t, testRw, am.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second)) + require.NoErrorf(err, "unexpected error verifying oplog entry: %s", err) + + found, err := repo.LookupAuthMethod(testCtx, am.PublicId) + require.NoError(err) + found.CreateTime = got.CreateTime + found.UpdateTime = got.UpdateTime + found.Version = got.Version + TestSortAuthMethods(t, []*AuthMethod{found, am}) + assert.Empty(cmp.Diff(found.AuthMethod, am.AuthMethod, protocmp.Transform())) + }) + } +} + +type mockGetWrapperer struct { + // kms is the underlying kms which is used to provide the mock's default + // behavior + kms kms.GetWrapperer + + // getErr is a mock value to return for the GetWrapper(...) operation + getErr error + + returnOplogWrapper wrapping.Wrapper + returnDbWrapper wrapping.Wrapper +} + +func (m *mockGetWrapperer) GetWrapper(ctx context.Context, scopeId string, purpose kms.KeyPurpose, opt ...kms.Option) (wrapping.Wrapper, error) { + switch { + case purpose == kms.KeyPurposeOplog && m.returnOplogWrapper != nil: + return m.returnOplogWrapper, nil + case purpose == kms.KeyPurposeDatabase && m.returnDbWrapper != nil: + return m.returnDbWrapper, nil + case m.getErr != nil: + return nil, m.getErr + default: + return m.kms.GetWrapper(ctx, scopeId, purpose, opt...) + } +} diff --git a/internal/auth/ldap/repository_auth_method_delete.go b/internal/auth/ldap/repository_auth_method_delete.go new file mode 100644 index 0000000000..85e0787e0c --- /dev/null +++ b/internal/auth/ldap/repository_auth_method_delete.go @@ -0,0 +1,62 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "fmt" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" +) + +// DeleteAuthMethod will delete the auth method from the repository. It is +// idempotent so if the auth method was not found, return 0 (no rows affected) +// and nil. No options are currently supported. +func (r *Repository) DeleteAuthMethod(ctx context.Context, publicId string, _ ...Option) (int, error) { + const op = "ldap.(Repository).DeleteAuthMethod" + if publicId == "" { + return db.NoRowsAffected, errors.New(ctx, errors.InvalidPublicId, op, "missing public id") + } + am, err := r.LookupAuthMethod(ctx, publicId) + if err != nil { + return db.NoRowsAffected, errors.Wrap(ctx, err, op) + } + if am == nil { + // already deleted and this is not an error. + return db.NoRowsAffected, nil + } + + oplogWrapper, err := r.kms.GetWrapper(ctx, am.ScopeId, kms.KeyPurposeOplog) + if err != nil { + return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) + } + var rowsDeleted int + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + cp := am.clone() + md, err := cp.oplog(ctx, oplog.OpType_OP_TYPE_DELETE) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to generate oplog metadata")) + } + rowsDeleted, err = w.Delete(ctx, cp, db.WithOplog(oplogWrapper, md)) + if err != nil { + return err + } + if rowsDeleted > 1 { + return errors.New(ctx, errors.MultipleRecords, op, "more than 1 auth method would have been deleted") + } + return nil + }, + ) + if err != nil { + return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to delete %s", publicId))) + } + return rowsDeleted, nil +} diff --git a/internal/auth/ldap/repository_auth_method_delete_test.go b/internal/auth/ldap/repository_auth_method_delete_test.go new file mode 100644 index 0000000000..22c9c30257 --- /dev/null +++ b/internal/auth/ldap/repository_auth_method_delete_test.go @@ -0,0 +1,186 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "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" +) + +func TestRepository_DeleteAuthMethod(t *testing.T) { + testConn, _ := db.TestSetup(t, "postgres") + testRw := db.New(testConn) + testWrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, testConn, testWrapper) + testCtx := context.Background() + + tests := []struct { + name string + reader db.Reader + writer db.Writer + kms kms.GetWrapperer + authMethod *AuthMethod + wantRowsDeleted int + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "valid", + reader: testRw, + writer: testRw, + kms: testKms, + authMethod: func() *AuthMethod { + org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper)) + databaseWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}, WithOperationalState(testCtx, InactiveState)) + }(), + wantRowsDeleted: 1, + }, + { + name: "no-public-id", + reader: testRw, + writer: testRw, + kms: testKms, + authMethod: func() *AuthMethod { am := AllocAuthMethod(); return &am }(), + wantErrMatch: errors.T(errors.InvalidPublicId), + wantErrContains: "missing public id", + }, + { + name: "not-found", + reader: testRw, + writer: testRw, + kms: testKms, + authMethod: func() *AuthMethod { + am := AllocAuthMethod() + var err error + am.PublicId, err = newAuthMethodId(testCtx) + require.NoError(t, err) + return &am + }(), + }, + { + name: "lookup-err", + reader: func() db.Reader { + conn, mock := db.TestSetupWithMock(t) + mock.ExpectQuery(`SELECT`).WillReturnError(errors.New(context.Background(), errors.Internal, "test", "lookup-error")) + return db.New(conn) + }(), + writer: testRw, + kms: testKms, + authMethod: func() *AuthMethod { + org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper)) + databaseWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}, WithOperationalState(testCtx, InactiveState)) + }(), + wantErrMatch: errors.T(errors.Internal), + wantErrContains: "lookup-err", + }, + { + name: "delete-err", + reader: testRw, + writer: func() db.Writer { + conn, mock := db.TestSetupWithMock(t) + mock.ExpectBegin() + mock.ExpectQuery(`SELECT`).WillReturnError(errors.New(context.Background(), errors.Internal, "test", "delete-error")) + mock.ExpectRollback() + return db.New(conn) + }(), + kms: testKms, + authMethod: func() *AuthMethod { + org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper)) + databaseWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}, WithOperationalState(testCtx, InactiveState)) + }(), + wantErrMatch: errors.T(errors.Internal), + wantErrContains: "delete-err", + }, + { + name: "multiple-rows-err", + reader: testRw, + writer: func() db.Writer { + conn, mock := db.TestSetupWithMock(t) + mock.ExpectBegin() + mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "version"}).AddRow("1", 1)) // oplog: get ticket + mock.ExpectExec(`DELETE`).WillReturnResult(sqlmock.NewResult(0, 10)) + mock.ExpectQuery(`INSERT`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("1")) // oplog: insert metadata + mock.ExpectQuery(`INSERT`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("1")) // oplog: insert entry + mock.ExpectExec(`UPDATE`).WillReturnResult(sqlmock.NewResult(0, 1)) // oplog: update ticket version + mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "version"}).AddRow("1", 1)) // oplog: get ticket again + mock.ExpectRollback() + return db.New(conn) + }(), + kms: testKms, + authMethod: func() *AuthMethod { + org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper)) + databaseWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}, WithOperationalState(testCtx, InactiveState)) + }(), + wantErrMatch: errors.T(errors.MultipleRecords), + wantErrContains: "more than 1 auth method would have been deleted", + }, + { + name: "getWrapper-err", + reader: testRw, + writer: testRw, + kms: &mockGetWrapperer{ + getErr: errors.New(testCtx, errors.Encrypt, "test", "get-db-wrapper-err"), + }, + authMethod: func() *AuthMethod { + org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper)) + databaseWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}, WithOperationalState(testCtx, InactiveState)) + }(), + wantErrMatch: errors.T(errors.Encrypt), + wantErrContains: "unable to get oplog wrapper", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(testCtx, tc.reader, tc.writer, tc.kms) + require.NoError(err) + deletedRows, err := repo.DeleteAuthMethod(testCtx, tc.authMethod.PublicId) + if tc.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tc.wantErrMatch, err), "want err: %q got: %q", tc.wantErrMatch.Msg, err) + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + + assert.Equalf(0, deletedRows, "expected 0 deleted rows and got %d", deletedRows) + + err := db.TestVerifyOplog(t, testRw, tc.authMethod.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second)) + require.Errorf(err, "should not have found oplog entry for %s", tc.authMethod.PublicId) + assert.Truef(errors.Match(errors.T(errors.RecordNotFound), err), "expected error code %s and got %s", errors.RecordNotFound, err) + + return + } + require.NoError(err) + assert.Equalf(tc.wantRowsDeleted, deletedRows, "expected rows deleted == %d and got %d", tc.wantRowsDeleted, deletedRows) + + if tc.wantRowsDeleted > 0 { + err = db.TestVerifyOplog(t, testRw, tc.authMethod.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second)) + require.NoErrorf(err, "unexpected error verifying oplog entry: %s", err) + } + found, err := repo.LookupAuthMethod(testCtx, tc.authMethod.PublicId) + require.NoError(err) + assert.Nil(found) + }) + } +} diff --git a/internal/auth/ldap/repository_auth_method_read.go b/internal/auth/ldap/repository_auth_method_read.go new file mode 100644 index 0000000000..a4bccc38c4 --- /dev/null +++ b/internal/auth/ldap/repository_auth_method_read.go @@ -0,0 +1,248 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/go-kms-wrapping/v2/extras/structwrapping" +) + +// LookupAuthMethod will lookup an auth method in the repo, along with its +// associated Value Objects of SigningAlgs, CallbackUrls, AudClaims and +// Certificates. If it's not found, it will return nil, nil. The +// WithUnauthenticatedUser options is supported and all other options are +// ignored. +func (r *Repository) LookupAuthMethod(ctx context.Context, publicId string, opt ...Option) (*AuthMethod, error) { + const op = "ldap.(Repository).LookupAuthMethod" + if publicId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing public id") + } + opts, err := getOpts(opt...) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + return r.lookupAuthMethod(ctx, publicId, WithUnauthenticatedUser(ctx, opts.withUnauthenticatedUser)) +} + +// ListAuthMethods returns a slice of AuthMethods for the scopeId. The +// WithUnauthenticatedUser, WithLimit and WithOrder options are supported and +// all other options are ignored. +func (r *Repository) ListAuthMethods(ctx context.Context, scopeIds []string, opt ...Option) ([]*AuthMethod, error) { + const op = "ldap.(Repository).ListAuthMethods" + if len(scopeIds) == 0 { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope IDs") + } + authMethods, err := r.getAuthMethods(ctx, "", scopeIds, opt...) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + return authMethods, nil +} + +// lookupAuthMethod will lookup a single auth method +func (r *Repository) lookupAuthMethod(ctx context.Context, authMethodId string, opt ...Option) (*AuthMethod, error) { + const op = "ldap.(Repository).lookupAuthMethod" + var err error + ams, err := r.getAuthMethods(ctx, authMethodId, nil, opt...) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + switch { + case len(ams) == 0: + return nil, nil // not an error to return no rows for a "lookup" + case len(ams) > 1: + return nil, errors.New(ctx, errors.NotSpecificIntegrity, op, fmt.Sprintf("%s matched more than 1 ", authMethodId)) + default: + return ams[0], nil + } +} + +// getAuthMethods allows the caller to either lookup a specific AuthMethod via +// its id or search for a set AuthMethods within a set of scopes. Passing both +// scopeIds and a authMethod is an error. The WithUnauthenticatedUser, +// WithLimit and WithOrder options are supported and all other options are +// ignored. +// +// The AuthMethod returned has its value objects populated (SigningAlgs, +// CallbackUrls, AudClaims and Certificates). The AuthMethod returned has its +// IsPrimaryAuthMethod bool set. +// +// When no record is found it returns nil, nil +func (r *Repository) getAuthMethods(ctx context.Context, authMethodId string, scopeIds []string, opt ...Option) ([]*AuthMethod, error) { + const op = "ldap.(Repository).getAuthMethods" + if authMethodId == "" && len(scopeIds) == 0 { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing search criteria: both auth method id and scope ids are empty") + } + if authMethodId != "" && len(scopeIds) > 0 { + return nil, errors.New(ctx, errors.InvalidParameter, op, "searching for both an auth method id and scope ids is not supported") + } + + const aggregateDelimiter = "|" + + dbArgs := []db.Option{} + opts, err := getOpts(opt...) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + limit := r.defaultLimit + if opts.withLimit != 0 { + // non-zero signals an override of the default limit for the repo. + limit = opts.withLimit + } + dbArgs = append(dbArgs, db.WithLimit(limit)) + + if opts.withOrderByCreateTime { + if opts.ascending { + dbArgs = append(dbArgs, db.WithOrder("create_time asc")) + } else { + dbArgs = append(dbArgs, db.WithOrder("create_time")) + } + } + + var args []any + var where []string + switch { + case authMethodId != "": + where, args = append(where, "public_id = ?"), append(args, authMethodId) + default: + where, args = append(where, "scope_id in(?)"), append(args, scopeIds) + } + + if opts.withUnauthenticatedUser { + // the caller is asking for a list of auth methods which can be returned + // to unauthenticated users (so they can authen). + where, args = append(where, "state = ?"), append(args, string(ActivePublicState)) + } + + var aggAuthMethods []*authMethodAgg + err = r.reader.SearchWhere(ctx, &aggAuthMethods, strings.Join(where, " and "), args, dbArgs...) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + if len(aggAuthMethods) == 0 { // we're done if nothing is found. + return nil, nil + } + + authMethods := make([]*AuthMethod, 0, len(aggAuthMethods)) + for _, agg := range aggAuthMethods { + + ccKey := struct { + Ct []byte `wrapping:"ct,certificate_key"` + Pt []byte `wrapping:"pt,certificate_key"` + }{Ct: agg.ClientCertificateKey} + if agg.ClientCertificateKey != nil { + ccWrapper, err := r.kms.GetWrapper(ctx, agg.ScopeId, kms.KeyPurposeDatabase, kms.WithKeyId(agg.ClientCertificateKeyId)) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("failed to get database wrapper for client certificate key")) + } + if err := structwrapping.UnwrapStruct(ctx, ccWrapper, &ccKey); err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt), errors.WithMsg("failed to decrypt client certificate key")) + } + } + bindPassword := struct { + Ct []byte `wrapping:"ct,password"` + Pt []byte `wrapping:"pt,password"` + }{Ct: agg.BindPassword} + if agg.BindPassword != nil { + bindWrapper, err := r.kms.GetWrapper(ctx, agg.ScopeId, kms.KeyPurposeDatabase, kms.WithKeyId(agg.BindKeyId)) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("failed to get database wrapper for bind password")) + } + if err := structwrapping.UnwrapStruct(ctx, bindWrapper, &bindPassword); err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt), errors.WithMsg("failed to decrypt bind password")) + } + } + am := AllocAuthMethod() + am.PublicId = agg.PublicId + am.ScopeId = agg.ScopeId + am.IsPrimaryAuthMethod = agg.IsPrimaryAuthMethod + am.Name = agg.Name + am.Description = agg.Description + am.CreateTime = agg.CreateTime + am.UpdateTime = agg.UpdateTime + am.Version = agg.Version + am.OperationalState = agg.State + am.StartTls = agg.StartTLS + am.InsecureTls = agg.InsecureTLS + am.DiscoverDn = agg.DiscoverDn + am.AnonGroupSearch = agg.AnonGroupSearch + am.EnableGroups = agg.EnableGroups + am.UseTokenGroups = agg.UseTokenGroups + am.UpnDomain = agg.UpnDomain + if agg.Urls != "" { + am.Urls = strings.Split(agg.Urls, aggregateDelimiter) + } + if agg.Certs != "" { + am.Certificates = strings.Split(agg.Certs, aggregateDelimiter) + } + am.UserDn = agg.UserDn + am.UserAttr = agg.UserAttr + am.UserFilter = agg.UserFilter + am.GroupDn = agg.GroupDn + am.GroupAttr = agg.GroupAttr + am.GroupFilter = agg.GroupFilter + am.ClientCertificateKey = ccKey.Pt + am.ClientCertificateKeyHmac = agg.ClientCertificateKeyHmac + am.ClientCertificate = string(agg.ClientCertificateCert) + am.BindDn = agg.BindDn + am.BindPassword = string(bindPassword.Pt) + am.BindPasswordHmac = agg.BindPasswordHmac + if agg.AccountAttributeMap != "" { + am.AccountAttributeMaps = strings.Split(agg.AccountAttributeMap, aggregateDelimiter) + } + + authMethods = append(authMethods, &am) + } + return authMethods, nil +} + +// authMethodAgg is a view that aggregates the auth method's value objects. If +// the value object can have multiple values like Urls and Certs, then the +// string field is delimited with the aggregateDelimiter of "|" +type authMethodAgg struct { + PublicId string `gorm:"primary_key"` + ScopeId string + IsPrimaryAuthMethod bool + Name string + Description string + CreateTime *timestamp.Timestamp + UpdateTime *timestamp.Timestamp + Version uint32 + State string + StartTLS bool + InsecureTLS bool + DiscoverDn bool + AnonGroupSearch bool + UpnDomain string + Urls string + Certs string + UserDn string + UserAttr string + UserFilter string + EnableGroups bool + UseTokenGroups bool + GroupDn string + GroupAttr string + GroupFilter string + ClientCertificateKey []byte + ClientCertificateKeyHmac []byte + ClientCertificateKeyId string + ClientCertificateCert []byte + BindDn string + BindPassword []byte + BindPasswordHmac []byte + BindKeyId string + AccountAttributeMap string +} + +// TableName returns the table name for gorm +func (agg *authMethodAgg) TableName() string { return "ldap_auth_method_with_value_obj" } diff --git a/internal/auth/ldap/repository_auth_method_read_test.go b/internal/auth/ldap/repository_auth_method_read_test.go new file mode 100644 index 0000000000..8f0da5018c --- /dev/null +++ b/internal/auth/ldap/repository_auth_method_read_test.go @@ -0,0 +1,357 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "crypto/ed25519" + "crypto/rand" + "crypto/x509" + "sort" + "testing" + + "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/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRepository_LookupAuthMethod(t *testing.T) { + testConn, _ := db.TestSetup(t, "postgres") + testRw := db.New(testConn) + testWrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, testConn, testWrapper) + testCtx := context.Background() + org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper)) + orgDbWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + amInactive := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId, []string{"ldaps://ldap1.alice.com"}, WithOperationalState(testCtx, InactiveState)) + amActivePriv := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId, []string{"ldaps://ldap2.alice.com"}, WithOperationalState(testCtx, ActivePrivateState)) + amActivePub := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId, []string{"ldaps://ldap3.alice.com"}, WithOperationalState(testCtx, ActivePublicState)) + amActivePub.IsPrimaryAuthMethod = true + iam.TestSetPrimaryAuthMethod(t, iam.TestRepo(t, testConn, testWrapper), org, amActivePub.PublicId) + + amId, err := newAuthMethodId(testCtx) + require.NoError(t, err) + tests := []struct { + name string + in string + opt []Option + want *AuthMethod + wantIsPrimary bool + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "missing-public-id", + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing public id", + }, + { + name: "invalid-opts", + in: amActivePub.GetPublicId(), + opt: []Option{WithBindCredential(testCtx, "", "")}, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing both dn and password", + }, + { + name: "non-existing-auth-method-id", + in: amId, + }, + { + name: "existing-auth-method-id", + in: amActivePriv.GetPublicId(), + want: amActivePriv, + }, + { + name: "unauthenticated-user-not-found-using-active-priv", + in: amActivePriv.GetPublicId(), + opt: []Option{WithUnauthenticatedUser(testCtx, true)}, + want: nil, + }, + { + name: "unauthenticated-user-found-active-pub", + in: amActivePub.GetPublicId(), + opt: []Option{WithUnauthenticatedUser(testCtx, true)}, + want: amActivePub, + wantIsPrimary: true, + }, + { + name: "unauthenticated-user-found-inactive", + in: amInactive.GetPublicId(), + opt: []Option{WithUnauthenticatedUser(testCtx, true)}, + want: nil, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(testCtx, testRw, testRw, testKms) + assert.NoError(err) + require.NotNil(repo) + got, err := repo.LookupAuthMethod(testCtx, tc.in, tc.opt...) + if tc.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tc.wantErrMatch, err), "want err code: %q got: %q", tc.wantErrMatch, err) + assert.Nil(got) + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.EqualValues(tc.want, got) + }) + } +} + +func TestRepository_getAuthMethods(t *testing.T) { + testConn, _ := db.TestSetup(t, "postgres") + testRw := db.New(testConn) + testWrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, testConn, testWrapper) + testCtx := context.Background() + + tests := []struct { + name string + setupFn func() (authMethodId string, scopeIds []string, want []*AuthMethod) + opt []Option + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "valid-multi-scopes", + setupFn: func() (string, []string, []*AuthMethod) { + org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper)) + orgDBWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + org2, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper)) + orgDBWrapper2, err := testKms.GetWrapper(context.Background(), org2.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + // make a test auth method with all options + am1a := TestAuthMethod(t, testConn, orgDBWrapper, org.PublicId, []string{"ldaps://ldap1.alice.com"}, WithOperationalState(testCtx, InactiveState)) + am1b := TestAuthMethod(t, testConn, orgDBWrapper, org.PublicId, []string{"ldaps://ldap2.alice.com"}, WithOperationalState(testCtx, InactiveState)) + am2 := TestAuthMethod(t, testConn, orgDBWrapper2, org2.PublicId, []string{"ldaps://ldap3.alice.com"}, WithOperationalState(testCtx, InactiveState)) + return "", []string{am1a.ScopeId, am1b.ScopeId, am2.ScopeId}, []*AuthMethod{am1a, am1b, am2} + }, + }, + { + name: "valid-single-scopes", + setupFn: func() (string, []string, []*AuthMethod) { + org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper)) + orgDBWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + am1a := TestAuthMethod(t, testConn, orgDBWrapper, org.PublicId, []string{"ldaps://ldap1.alice.com"}, WithOperationalState(testCtx, InactiveState)) + am1b := TestAuthMethod(t, testConn, orgDBWrapper, org.PublicId, []string{"ldaps://ldap2.alice.com"}, WithOperationalState(testCtx, InactiveState)) + + return "", []string{am1a.ScopeId}, []*AuthMethod{am1a, am1b} + }, + }, + { + name: "valid-auth-method-id-all-opts", + setupFn: func() (string, []string, []*AuthMethod) { + org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper)) + orgDbWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + testCert, _ := TestGenerateCA(t, "localhost") + _, testPrivKey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + derPrivKey, err := x509.MarshalPKCS8PrivateKey(testPrivKey) + require.NoError(t, err) + + am := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId, []string{"ldaps://alice.com"}, + WithName(testCtx, "test-name"), + WithDescription(testCtx, "test-description"), + WithStartTLS(testCtx), + WithInsecureTLS(testCtx), + WithDiscoverDn(testCtx), + WithAnonGroupSearch(testCtx), + WithUpnDomain(testCtx, "alice.com"), + WithUserDn(testCtx, "user-dn"), + WithUserAttr(testCtx, "user-attr"), + WithUserFilter(testCtx, "user-filter"), + WithEnableGroups(testCtx), + WithUseTokenGroups(testCtx), + WithGroupDn(testCtx, "group-dn"), + WithGroupAttr(testCtx, "group-attr"), + WithGroupFilter(testCtx, "group-filter"), + WithBindCredential(testCtx, "bind-dn", "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, "displayName": ToFullNameAttribute}), + ) + return am.PublicId, nil, []*AuthMethod{am} + }, + }, + { + name: "with-limits", + setupFn: func() (string, []string, []*AuthMethod) { + org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper)) + orgDBWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + am1a := TestAuthMethod(t, testConn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}) + _ = TestAuthMethod(t, testConn, orgDBWrapper, org.PublicId, []string{"ldaps://alice2.com"}) + + return "", []string{am1a.ScopeId}, []*AuthMethod{am1a} + }, + opt: []Option{WithLimit(testCtx, 1), WithOrderByCreateTime(testCtx, true)}, + }, + { + name: "unauthenticated-user", + setupFn: func() (string, []string, []*AuthMethod) { + org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper)) + databaseWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + _ = TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1.alice.com"}, WithOperationalState(testCtx, InactiveState)) + _ = TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap2.alice.com"}, WithOperationalState(testCtx, ActivePrivateState)) + amActivePub := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap3.alice.com"}, WithOperationalState(testCtx, ActivePublicState)) + + return "", []string{amActivePub.ScopeId}, []*AuthMethod{amActivePub} + }, + opt: []Option{WithUnauthenticatedUser(testCtx, true)}, + }, + { + name: "not-found-auth-method-id", + setupFn: func() (string, []string, []*AuthMethod) { + return "not-a-valid-id", nil, nil + }, + }, + { + name: "not-found-scope-ids", + setupFn: func() (string, []string, []*AuthMethod) { + return "", []string{"not-valid-scope-1", "not-valid-scope-2"}, nil + }, + }, + { + name: "no-search-criteria", + setupFn: func() (string, []string, []*AuthMethod) { + return "", nil, nil + }, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing search criteria: both auth method id and scope ids are empty", + }, + { + name: "search-too-many", + setupFn: func() (string, []string, []*AuthMethod) { + return "auth-method-id", []string{"scope-id"}, nil + }, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "searching for both an auth method id and scope ids is not supported", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + r, err := NewRepository(testCtx, testRw, testRw, testKms) + require.NoError(err) + + authMethodId, scopeIds, want := tc.setupFn() + + got, err := r.getAuthMethods(testCtx, authMethodId, scopeIds, tc.opt...) + + if tc.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tc.wantErrMatch, err), "want err code: %q got: %q", tc.wantErrMatch, err) + assert.Nil(got) + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + TestSortAuthMethods(t, want) + TestSortAuthMethods(t, got) + assert.Equal(want, got) + }) + } +} + +// TestRepository_ListAuthMethods only covers error conditions, since all the +// search criteria testing is handled in the TestRepository_getAuthMethods unit +// tests. +func TestRepository_ListAuthMethods(t *testing.T) { + testConn, _ := db.TestSetup(t, "postgres") + testRw := db.New(testConn) + testWrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, testConn, testWrapper) + testCtx := context.Background() + iamRepo := iam.TestRepo(t, testConn, testWrapper) + + tests := []struct { + name string + setupFn func() (scopeIds []string, want []*AuthMethod, wantPrimaryAuthMethodId string) + opt []Option + wantErrMatch *errors.Template + }{ + { + name: "with-limits", + setupFn: func() ([]string, []*AuthMethod, string) { + org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper)) + orgDbWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + am1a := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId, []string{"ldaps://alice.com"}) + iam.TestSetPrimaryAuthMethod(t, iamRepo, org, am1a.PublicId) + am1a.IsPrimaryAuthMethod = true + + _ = TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId, []string{"ldaps://alice2.com"}) + + return []string{am1a.ScopeId}, []*AuthMethod{am1a}, am1a.PublicId + }, + opt: []Option{WithLimit(testCtx, 1), WithOrderByCreateTime(testCtx, true)}, + }, + { + name: "no-search-criteria", + setupFn: func() ([]string, []*AuthMethod, string) { + return nil, nil, "" + }, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(testCtx, testRw, testRw, testKms) + assert.NoError(err) + scopeIds, want, wantPrimaryAuthMethodId := tt.setupFn() + + got, err := repo.ListAuthMethods(testCtx, scopeIds, tt.opt...) + if tt.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tt.wantErrMatch, err), "want err code: %q got: %q", tt.wantErrMatch, err) + assert.Nil(got) + return + } + require.NoError(err) + sort.Slice(want, func(a, b int) bool { + return want[a].PublicId < want[b].PublicId + }) + sort.Slice(got, func(a, b int) bool { + return got[a].PublicId < got[b].PublicId + }) + assert.Equal(want, got) + if wantPrimaryAuthMethodId != "" { + found := false + for _, am := range got { + if am.PublicId == wantPrimaryAuthMethodId { + assert.Truef(am.IsPrimaryAuthMethod, "expected IsPrimaryAuthMethod to be true for: %s", am.PublicId) + if am.IsPrimaryAuthMethod { + found = true + } + } + } + assert.Truef(found, "expected to find primary id %s in: %+v", wantPrimaryAuthMethodId, got) + } else { + for _, am := range got { + assert.Falsef(am.IsPrimaryAuthMethod, "did not expect %s to be IsPrimaryAuthMethod", am.PublicId) + } + } + }) + } +} diff --git a/internal/auth/ldap/repository_auth_method_update.go b/internal/auth/ldap/repository_auth_method_update.go new file mode 100644 index 0000000000..b3ba337024 --- /dev/null +++ b/internal/auth/ldap/repository_auth_method_update.go @@ -0,0 +1,738 @@ +// 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 +} diff --git a/internal/auth/ldap/repository_auth_method_update_test.go b/internal/auth/ldap/repository_auth_method_update_test.go new file mode 100644 index 0000000000..ed3d6d2547 --- /dev/null +++ b/internal/auth/ldap/repository_auth_method_update_test.go @@ -0,0 +1,1188 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +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, + }), + ) + }, + 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), + } + 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, + }, + 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 + 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. + ) + }, + updateWith: func(orig *AuthMethod) *AuthMethod { + am := AllocAuthMethod() + am.PublicId = orig.PublicId + return &am + }, + fieldMasks: []string{ + NameField, + DescriptionField, + StartTlsField, + InsecureTlsField, + DiscoverDnField, + AnonGroupSearchField, + UpnDomainField, + EnableGroupsField, + UseTokenGroupsField, + }, + 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: "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, + }), + ) + }, + 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"} + return &am + }, + fieldMasks: []string{ + UrlsField, + UserDnField, + UserAttrField, + UserFilterField, + GroupDnField, + GroupAttrField, + GroupFilterField, + BindDnField, + BindPasswordField, + CertificatesField, + ClientCertificateField, + ClientCertificateKeyField, + AccountAttributeMapsField, + }, + 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 + 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, + }), + ) + }, + 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, + }, + 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 + 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) + }) + } +} diff --git a/internal/auth/ldap/repository_authenticate.go b/internal/auth/ldap/repository_authenticate.go new file mode 100644 index 0000000000..f417adcf77 --- /dev/null +++ b/internal/auth/ldap/repository_authenticate.go @@ -0,0 +1,153 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "encoding/json" + "fmt" + "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/cap/ldap" +) + +const ( + DefaultEmailAttribute = "email" + DefaultFullNameAttribute = "fullName" + DefaultRequestTimeout = 5 // seconds +) + +// Authenticate authenticates loginName and password via the auth method's +// configured LDAP service. The account for the loginName is returned if +// authentication is successful. Returns nil if authentication fails. +// +// If the AuthMethod.EnableGroups is true, then the authenticated user's groups +// will be returned in account. +// +// Authenticate will update the stored values for the authenticated user's +// Account: FullName, Email, Dn, EntryAttributes, and MemberOfGroups. +// +// Note: the auth_method table uses public id as its PK, so there's no need a +// scope id parameter. +func (r *Repository) Authenticate(ctx context.Context, authMethodId, loginName, password string) (*Account, error) { + const op = "ldap.(Repository).Authenticate" + switch { + case authMethodId == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id", errors.WithoutEvent()) + case loginName == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing login name", errors.WithoutEvent()) + case password == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing password", errors.WithoutEvent()) + } + + // lookup auth method + am, err := r.lookupAuthMethod(ctx, authMethodId) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to lookup auth method id: %q", authMethodId)) + } + if am == nil { + return nil, errors.New(ctx, errors.RecordNotFound, op, fmt.Sprintf("auth method id %q not found", authMethodId)) + } + + // config cap ldap provider + client, err := ldap.NewClient(ctx, &ldap.ClientConfig{ + IncludeUserAttributes: true, + StartTLS: am.StartTls, + InsecureTLS: am.InsecureTls, + DiscoverDN: am.DiscoverDn, + AnonymousGroupSearch: am.AnonGroupSearch, + UPNDomain: am.UpnDomain, + URLs: am.Urls, + UserDN: am.UserDn, + UserFilter: am.UserFilter, + UserAttr: am.UserAttr, + IncludeUserGroups: am.EnableGroups, + UseTokenGroups: am.UseTokenGroups, + GroupDN: am.GroupDn, + GroupAttr: am.GroupAttr, + GroupFilter: am.GroupFilter, + Certificates: am.Certificates, + ClientTLSKey: string(am.ClientCertificateKey), + ClientTLSCert: am.ClientCertificate, + BindDN: am.BindDn, + BindPassword: am.BindPassword, + RequestTimeout: DefaultRequestTimeout, + }) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to initialize ldap client with auth method retrieved from database")) + } + defer client.Close(ctx) + + // authen user + authResult, err := client.Authenticate(ctx, loginName, password) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("authenticate failed")) + } + acct, err := NewAccount(ctx, am.ScopeId, am.PublicId, loginName) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + acctId, err := newAccountId(ctx, authMethodId, loginName) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + acct.PublicId = acctId + acct.Dn = authResult.UserDN + + if authResult.UserAttributes != nil { + found, email := caseInsensitiveAttributeSearch(DefaultEmailAttribute, authResult.UserAttributes) + if found { + acct.Email = email[0] + } + found, fullName := caseInsensitiveAttributeSearch(DefaultFullNameAttribute, authResult.UserAttributes) + if found { + acct.FullName = fullName[0] + } + } + if len(authResult.Groups) > 0 { + encodedGroups, err := json.Marshal(authResult.Groups) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to encode user groups")) + } + acct.MemberOfGroups = string(encodedGroups) + } + + databaseWrapper, err := r.kms.GetWrapper(ctx, am.ScopeId, kms.KeyPurposeDatabase) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + md, err := acct.oplog(ctx, oplog.OpType_OP_TYPE_CREATE) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + // upsert account + if err := r.writer.Create( + ctx, + acct, + db.WithOnConflict(&db.OnConflict{ + Target: db.Columns{"public_id"}, // id is predictable and uses both auth method id and login name for inputs + Action: db.SetColumns([]string{"full_name", "email", "dn", "member_of_groups"}), + }), + db.WithOplog(databaseWrapper, md), + ); err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create/update ldap account")) + } + + // return account + return acct, nil +} + +func caseInsensitiveAttributeSearch(attrName string, attributes map[string][]string) (bool, []string) { + for k, v := range attributes { + if strings.EqualFold(k, attrName) { + return true, v + } + } + return false, nil +} diff --git a/internal/auth/ldap/repository_authenticate_test.go b/internal/auth/ldap/repository_authenticate_test.go new file mode 100644 index 0000000000..7b76929c18 --- /dev/null +++ b/internal/auth/ldap/repository_authenticate_test.go @@ -0,0 +1,298 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/boundary/internal/auth/ldap/store" + "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/cap/ldap" + "github.com/hashicorp/go-hclog" + "github.com/jimlambrt/gldap" + "github.com/jimlambrt/gldap/testdirectory" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestRepository_authenticate(t *testing.T) { + t.Parallel() + testCtx := context.Background() + + rootWrapper := db.TestWrapper(t) + + testConn, _ := db.TestSetup(t, "postgres") + testRw := db.New(testConn) + + testKms := kms.TestKms(t, testConn, rootWrapper) + + testRepo, err := NewRepository(testCtx, testRw, testRw, testKms) + require.NoError(t, err) + + iamRepo := iam.TestRepo(t, testConn, rootWrapper) + org, _ := iam.TestScopes(t, iamRepo) + orgDbWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + logger := hclog.New(&hclog.LoggerOptions{ + Name: "test-logger", + Level: hclog.Error, + }) + td := testdirectory.Start(t, + testdirectory.WithDefaults(t, &testdirectory.Defaults{AllowAnonymousBind: true}), + testdirectory.WithLogger(t, logger), + ) + tdCerts, err := ParseCertificates(testCtx, td.Cert()) + require.NoError(t, err) + + testAm := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId, + []string{fmt.Sprintf("ldaps://127.0.0.1:%d", td.Port())}, + WithCertificates(testCtx, tdCerts...), + WithDiscoverDn(testCtx), + WithEnableGroups(testCtx), + WithUserDn(testCtx, testdirectory.DefaultUserDN), + WithGroupDn(testCtx, testdirectory.DefaultGroupDN), + ) + + const ( + testLoginName = "alice" + testPassword = "password" + ) + + testAccount := TestAccount(t, testConn, testAm, testLoginName) + + groups := []*gldap.Entry{ + testdirectory.NewGroup(t, "admin", []string{"alice"}), + testdirectory.NewGroup(t, "admin", []string{"eve"}, testdirectory.WithDefaults(t, &testdirectory.Defaults{UPNDomain: "example.com"})), + } + tokenGroups := map[string][]*gldap.Entry{ + "S-1-1": { + testdirectory.NewGroup(t, "admin-token-group", []string{"alice"}), + }, + } + sidBytes, err := ldap.SIDBytes(1, 1) + require.NoError(t, err) + users := testdirectory.NewUsers(t, []string{"alice", "bob"}, testdirectory.WithMembersOf(t, "admin"), testdirectory.WithTokenGroups(t, sidBytes)) + users = append( + users, + testdirectory.NewUsers( + t, + []string{"eve"}, + testdirectory.WithDefaults(t, &testdirectory.Defaults{UPNDomain: "example.com"}), + testdirectory.WithMembersOf(t, "admin"))..., + ) + // add some attributes that we always want to filter out of an AuthResult, + // so if we ever start seeing tests fail because of them; we know that we've + // messed up the default filtering + for _, u := range users { + u.Attributes = append(u.Attributes, + gldap.NewEntryAttribute(ldap.DefaultADUserPasswordAttribute, []string{"password"}), + gldap.NewEntryAttribute(ldap.DefaultOpenLDAPUserPasswordAttribute, []string{"password"}), + gldap.NewEntryAttribute("fullName", []string{"test-full-name"}), + ) + } + td.SetUsers(users...) + td.SetGroups(groups...) + td.SetTokenGroups(tokenGroups) + + tests := []struct { + name string + ctx context.Context + repo *Repository + authMethodId string + loginName string + password string + want func(got *Account) *Account + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "success-existing-account", + ctx: testCtx, + repo: testRepo, + authMethodId: testAm.PublicId, + loginName: testAccount.LoginName, + password: testPassword, + want: func(got *Account) *Account { + a := &Account{Account: &store.Account{ + AuthMethodId: testAm.PublicId, + ScopeId: testAccount.ScopeId, + PublicId: testAccount.PublicId, + Version: testAccount.Version, + Dn: "cn=alice,ou=people,dc=example,dc=org", + Email: "alice@example.com", + FullName: "test-full-name", + LoginName: "alice", + MemberOfGroups: "[\"cn=admin,ou=groups,dc=example,dc=org\"]", + }} + return a + }, + }, + { + name: "success-new-account-with-no-groups", + ctx: testCtx, + repo: testRepo, + authMethodId: testAm.PublicId, + loginName: "bob", + password: testPassword, + want: func(got *Account) *Account { + a := &Account{Account: &store.Account{ + AuthMethodId: testAm.PublicId, + ScopeId: testAm.ScopeId, + PublicId: got.PublicId, + Version: got.Version, + Dn: "cn=bob,ou=people,dc=example,dc=org", + Email: "bob@example.com", + FullName: "test-full-name", + LoginName: "bob", + }} + return a + }, + }, + { + name: "missing-auth-method-id", + ctx: testCtx, + repo: testRepo, + loginName: "alice", + password: testPassword, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing auth method id", + }, + { + name: "missing-login-name", + ctx: testCtx, + repo: testRepo, + authMethodId: testAm.PublicId, + password: testPassword, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing login name", + }, + { + name: "missing-password", + ctx: testCtx, + repo: testRepo, + authMethodId: testAm.PublicId, + loginName: "alice", + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing password", + }, + { + name: "auth-method-id-not-found", + ctx: testCtx, + repo: testRepo, + authMethodId: "auth-method-id-not-found", + loginName: "alice", + password: testPassword, + wantErrMatch: errors.T(errors.RecordNotFound), + wantErrContains: "auth method id \"auth-method-id-not-found\" not found", + }, + { + name: "auth-method-id-lookup-err", + ctx: testCtx, + repo: func() *Repository { + conn, mock := db.TestSetupWithMock(t) + mock.ExpectQuery(`SELECT`).WillReturnError(fmt.Errorf("auth-method-id-lookup-err")) + rw := db.New(conn) + r, err := NewRepository(testCtx, rw, rw, testKms) + require.NoError(t, err) + return r + }(), + authMethodId: testAm.PublicId, + loginName: "alice", + password: testPassword, + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "auth-method-id-lookup-err", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := tc.repo.Authenticate(tc.ctx, tc.authMethodId, tc.loginName, tc.password) + if tc.wantErrMatch != nil { + require.Error(err) + assert.Nil(got) + assert.Truef(errors.Match(tc.wantErrMatch, err), "unexpected error: %s", err.Error()) + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + require.NotNil(tc.want) + assert.NotEmpty(got.UpdateTime) + assert.NotEmpty(got.CreateTime) + w := tc.want(got) + w.UpdateTime = got.UpdateTime + w.CreateTime = got.CreateTime + assert.Empty(cmp.Diff(w, got, protocmp.Transform())) + }) + } + t.Run("use-token-groups", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + amWithTokenGroups := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId, + []string{fmt.Sprintf("ldaps://127.0.0.1:%d", td.Port())}, + WithCertificates(testCtx, tdCerts...), + WithDiscoverDn(testCtx), + WithEnableGroups(testCtx), + WithUserDn(testCtx, testdirectory.DefaultUserDN), + WithUseTokenGroups(testCtx), + ) + + got, err := testRepo.Authenticate(testCtx, amWithTokenGroups.PublicId, testLoginName, testPassword) + require.NoError(err) + assert.NotNil(got) + assert.NotEmpty(got.UpdateTime) + assert.NotEmpty(got.CreateTime) + w := &Account{Account: &store.Account{ + AuthMethodId: amWithTokenGroups.PublicId, + ScopeId: amWithTokenGroups.ScopeId, + PublicId: got.PublicId, + Version: got.Version, + Dn: "cn=alice,ou=people,dc=example,dc=org", + Email: "alice@example.com", + FullName: "test-full-name", + LoginName: "alice", + MemberOfGroups: "[\"cn=admin-token-group,ou=groups,dc=example,dc=org\"]", + }} + w.UpdateTime = got.UpdateTime + w.CreateTime = got.CreateTime + assert.Empty(cmp.Diff(w, got, protocmp.Transform())) + }) + t.Run("authenticate-err", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + amWithNoCerts := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId, + []string{fmt.Sprintf("ldaps://127.0.0.1:%d", td.Port())}, + WithDiscoverDn(testCtx), + WithEnableGroups(testCtx), + WithUserDn(testCtx, testdirectory.DefaultUserDN), + WithGroupDn(testCtx, testdirectory.DefaultGroupDN), + ) + got, err := testRepo.Authenticate(testCtx, amWithNoCerts.PublicId, testLoginName, testPassword) + require.Error(err) + assert.Contains(err.Error(), "authenticate failed") + assert.Contains(err.Error(), "failed to connect") + assert.Nil(got) + }) +} + +func Test_caseInsensitiveAttributeSearch(t *testing.T) { + t.Parallel() + assert := assert.New(t) + + found, values := caseInsensitiveAttributeSearch("fullName", map[string][]string{"fullName": {"eve"}}) + assert.True(found) + assert.Equal([]string{"eve"}, values) + + found, values = caseInsensitiveAttributeSearch("preferredName", map[string][]string{"fullName": {"eve"}}) + assert.False(found) + assert.Empty(values) +} diff --git a/internal/auth/ldap/repository_managed_group.go b/internal/auth/ldap/repository_managed_group.go new file mode 100644 index 0000000000..b7f939856d --- /dev/null +++ b/internal/auth/ldap/repository_managed_group.go @@ -0,0 +1,264 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "fmt" + "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" +) + +// CreateManagedGroup inserts an ManagedGroup, mg, into the repository and +// returns a new ManagedGroup containing its PublicId. mg is not changed. mg +// must contain a valid AuthMethodId. mg must not contain a PublicId. The +// PublicId is generated and assigned by this method. All options are ignored. +// +// Both mg.Name and mg.Description are optional. If mg.Name is set, it must be +// unique within mg.AuthMethodId. +func (r *Repository) CreateManagedGroup(ctx context.Context, scopeId string, mg *ManagedGroup, _ ...Option) (*ManagedGroup, error) { + const op = "ldap.(Repository).CreateManagedGroup" + switch { + case mg == nil: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing managed group") + case mg.ManagedGroup == nil: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing embedded managed group") + case mg.AuthMethodId == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id") + case len(mg.GroupNames) == 0: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing group names") + case mg.PublicId != "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "public id must be empty") + case scopeId == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope id") + } + + mg = mg.clone() + + id, err := newManagedGroupId(ctx) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + mg.PublicId = id + + oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeOplog) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"), errors.WithCode(errors.Encrypt)) + } + + oplogMetadata, err := mg.oplog(ctx, oplog.OpType_OP_TYPE_CREATE, scopeId) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to generate managed group oplog metadata")) + } + + var newManagedGroup *ManagedGroup + _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + newManagedGroup = mg.clone() + if err := w.Create(ctx, newManagedGroup, db.WithOplog(oplogWrapper, oplogMetadata)); err != nil { + return errors.Wrap(ctx, err, op) + } + return nil + }, + ) + + if err != nil { + if errors.IsUniqueError(err) { + return nil, errors.New(ctx, errors.NotUnique, op, fmt.Sprintf( + "in auth method %s: name %q already exists", + mg.AuthMethodId, mg.Name)) + } + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(mg.AuthMethodId)) + } + return newManagedGroup, nil +} + +// LookupManagedGroup will look up a managed group in the repository. If the managed group is not +// found, it will return nil, nil. All options are ignored. +func (r *Repository) LookupManagedGroup(ctx context.Context, withPublicId string, _ ...Option) (*ManagedGroup, error) { + const op = "ldap.(Repository).LookupManagedGroup" + if withPublicId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing public id") + } + a := AllocManagedGroup() + a.PublicId = withPublicId + if err := r.reader.LookupByPublicId(ctx, a); err != nil { + if errors.IsNotFoundError(err) { + return nil, nil + } + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for %s", withPublicId))) + } + return a, nil +} + +// ListManagedGroups in an auth method and supports WithLimit option. +func (r *Repository) ListManagedGroups(ctx context.Context, withAuthMethodId string, opt ...Option) ([]*ManagedGroup, error) { + const op = "ldap.(Repository).ListManagedGroups" + if withAuthMethodId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id") + } + opts, err := getOpts(opt...) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + limit := r.defaultLimit + if opts.withLimit != 0 { + // non-zero signals an override of the default limit for the repo. + limit = opts.withLimit + } + var mgs []*ManagedGroup + err = r.reader.SearchWhere(ctx, &mgs, "auth_method_id = ?", []any{withAuthMethodId}, db.WithLimit(limit)) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + return mgs, nil +} + +// DeleteManagedGroup deletes the managed group for the provided id from the +// repository returning a count of the number of records deleted. All options +// are ignored. +func (r *Repository) DeleteManagedGroup(ctx context.Context, scopeId, withPublicId string, opt ...Option) (int, error) { + const op = "ldap.(Repository).DeleteManagedGroup" + switch { + case withPublicId == "": + return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing public id") + case scopeId == "": + return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing scope id") + } + mg := AllocManagedGroup() + mg.PublicId = withPublicId + if err := r.reader.LookupById(ctx, mg); err != nil { + return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("managed group not found")) + } + oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeOplog) + if err != nil { + return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("unable to get oplog wrapper")) + } + + metadata, err := mg.oplog(ctx, oplog.OpType_OP_TYPE_DELETE, scopeId) + if err != nil { + return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to generate oplog metadata")) + } + + var rowsDeleted int + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) (err error) { + dMg := mg.clone() + rowsDeleted, err = w.Delete(ctx, dMg, db.WithOplog(oplogWrapper, metadata)) + if err != nil { + return errors.Wrap(ctx, err, op) + } + if rowsDeleted > 1 { + return errors.New(ctx, errors.MultipleRecords, op, "more than 1 resource would have been deleted") + } + return nil + }, + ) + + if err != nil { + return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(withPublicId)) + } + + return rowsDeleted, nil +} + +// UpdateManagedGroup updates the repository entry for mg.PublicId with the +// values in mg for the fields listed in fieldMaskPaths. It returns a new +// ManagedGroup containing the updated values and a count of the number of +// records updated. mg is not changed. +// +// mg must contain a valid PublicId. Only mg.Name, mg.Description, and mg.GroupNames +// can be updated. If mg.Name is set to a non-empty string, it must be unique +// within mg.AuthMethodId. +// +// An attribute of a will be set to NULL in the database if the attribute in a +// is the zero value and it is included in fieldMaskPaths. +func (r *Repository) UpdateManagedGroup(ctx context.Context, scopeId string, mg *ManagedGroup, version uint32, fieldMaskPaths []string, opt ...Option) (*ManagedGroup, int, error) { + const op = "ldap.(Repository).UpdateManagedGroup" + switch { + case mg == nil: + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing ManagedGroup") + case mg.ManagedGroup == nil: + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing embedded ManagedGroup") + case mg.PublicId == "": + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing public id") + case version == 0: + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version") + case scopeId == "": + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing scope id") + } + + for _, f := range fieldMaskPaths { + switch { + case strings.EqualFold(NameField, f): + case strings.EqualFold(DescriptionField, f): + case strings.EqualFold(GroupNamesField, f): + default: + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidFieldMask, op, f) + } + } + var dbMask, nullFields []string + dbMask, nullFields = dbw.BuildUpdatePaths( + map[string]any{ + NameField: mg.Name, + DescriptionField: mg.Description, + GroupNamesField: mg.GroupNames, + }, + fieldMaskPaths, + nil, + ) + if len(dbMask) == 0 && len(nullFields) == 0 { + return nil, db.NoRowsAffected, errors.New(ctx, errors.EmptyFieldMask, op, "missing field mask") + } + + oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeOplog) + if err != nil { + return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), + errors.WithMsg(("unable to get oplog wrapper"))) + } + + foundMg := AllocManagedGroup() + foundMg.PublicId = mg.PublicId + if err := r.reader.LookupById(ctx, foundMg); err != nil { + return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("managed group not found")) + } + metadata, err := foundMg.oplog(ctx, oplog.OpType_OP_TYPE_UPDATE, scopeId) + if err != nil { + return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to generate oplog metadata")) + } + var rowsUpdated int + var returnedManagedGroup *ManagedGroup + _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + returnedManagedGroup = mg.clone() + var err error + rowsUpdated, err = w.Update(ctx, returnedManagedGroup, dbMask, nullFields, db.WithOplog(oplogWrapper, metadata), db.WithVersion(&version)) + if err != nil { + return errors.Wrap(ctx, err, op) + } + if rowsUpdated > 1 { + return errors.New(ctx, errors.MultipleRecords, op, "more than 1 resource would have been updated") + } + return nil + }, + ) + + if err != nil { + if errors.IsUniqueError(err) { + return nil, db.NoRowsAffected, errors.New(ctx, errors.NotUnique, op, + fmt.Sprintf("name %s already exists: %s", mg.Name, mg.PublicId)) + } + return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(mg.PublicId)) + } + + return returnedManagedGroup, rowsUpdated, nil +} diff --git a/internal/auth/ldap/repository_managed_group_members.go b/internal/auth/ldap/repository_managed_group_members.go new file mode 100644 index 0000000000..67f7d5e8f2 --- /dev/null +++ b/internal/auth/ldap/repository_managed_group_members.go @@ -0,0 +1,83 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + + "github.com/hashicorp/boundary/internal/auth/oidc/store" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" +) + +// managedGroupMemberAccountTableName defines the default table name for a Managed Group +const managedGroupMemberAccountTableName = "auth_ldap_managed_group_member_account" + +// ManagedGroupMemberAccount contains a mapping between a managed group and a +// member account. +type ManagedGroupMemberAccount struct { + *store.ManagedGroupMemberAccount + tableName string +} + +// TableName returns the table name. +func (mg *ManagedGroupMemberAccount) TableName() string { + if mg.tableName != "" { + return mg.tableName + } + return managedGroupMemberAccountTableName +} + +// SetTableName sets the table name. +func (mg *ManagedGroupMemberAccount) SetTableName(n string) { + mg.tableName = n +} + +// ListManagedGroupMembershipsByMember lists managed group memberships via the +// member (account) ID and supports WithLimit option. +func (r *Repository) ListManagedGroupMembershipsByMember(ctx context.Context, withAcctId string, opt ...Option) ([]*ManagedGroupMemberAccount, error) { + const op = "ldap.(Repository).ListManagedGroupMembershipsByMember" + if withAcctId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing account id") + } + opts, err := getOpts(opt...) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + limit := r.defaultLimit + if opts.withLimit != 0 { + // non-zero signals an override of the default limit for the repo. + limit = opts.withLimit + } + var mgs []*ManagedGroupMemberAccount + err = r.reader.SearchWhere(ctx, &mgs, "member_id = ?", []any{withAcctId}, db.WithLimit(limit)) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + return mgs, nil +} + +// ListManagedGroupMembershipsByGroup lists managed group memberships via the +// group ID and supports WithLimit option. +func (r *Repository) ListManagedGroupMembershipsByGroup(ctx context.Context, withGroupId string, opt ...Option) ([]*ManagedGroupMemberAccount, error) { + const op = "ldap.(Repository).ListManagedGroupMembershipsByGroup" + if withGroupId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing managed group id") + } + opts, err := getOpts(opt...) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + limit := r.defaultLimit + if opts.withLimit != 0 { + // non-zero signals an override of the default limit for the repo. + limit = opts.withLimit + } + var mgs []*ManagedGroupMemberAccount + err = r.reader.SearchWhere(ctx, &mgs, "managed_group_id = ?", []any{withGroupId}, db.WithLimit(limit)) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + return mgs, nil +} diff --git a/internal/auth/ldap/repository_managed_group_members_test.go b/internal/auth/ldap/repository_managed_group_members_test.go new file mode 100644 index 0000000000..69affea645 --- /dev/null +++ b/internal/auth/ldap/repository_managed_group_members_test.go @@ -0,0 +1,233 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap_test + +import ( + "context" + "encoding/json" + "math/rand" + "testing" + + "github.com/hashicorp/boundary/internal/auth/ldap" + "github.com/hashicorp/boundary/internal/auth/oidc/store" + "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/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_ManagedGroupMemberships(t *testing.T) { + t.Parallel() + + testConn, _ := db.TestSetup(t, "postgres") + testRootWrapper := db.TestWrapper(t) + testRw := db.New(testConn) + + testKms := kms.TestKms(t, testConn, testRootWrapper) + iamRepo := iam.TestRepo(t, testConn, testRootWrapper) + + testCtx := context.Background() + testGlobalDbWrapper, err := testKms.GetWrapper(testCtx, "global", kms.KeyPurposeDatabase) + require.NoError(t, err) + + testAuthMethod := ldap.TestAuthMethod(t, testConn, testGlobalDbWrapper, "global", []string{"ldaps://ldap1"}) + testAuthMethodStatic := ldap.TestAuthMethod(t, testConn, testGlobalDbWrapper, "global", []string{"ldaps://ldap1"}) + + repo, err := ldap.NewRepository(testCtx, testRw, testRw, testKms) + require.NoError(t, err) + require.NotNil(t, repo) + + // make some groups with GroupNames which will initially match no accounts + mgs := make([]*ldap.ManagedGroup, 0, 100) + for i := 0; i < 100; i++ { + got := ldap.TestManagedGroup(t, testConn, testAuthMethod, []string{"foo", "bar"}) + mgs = append(mgs, got) + } + + testUser := iam.TestUser(t, iamRepo, "global") + staticAccount := ldap.TestAccount(t, testConn, testAuthMethodStatic, "test-static-login-name", ldap.WithMemberOfGroups(testCtx, "static")) + staticGroup := ldap.TestManagedGroup(t, testConn, testAuthMethod, []string{"static"}) + const staticMembershipCount = 1 + + testGroupNames := []string{"admin", "users"} + testGroupNamesEncodes, err := json.Marshal(testGroupNames) + require.NoError(t, err) + + // Fetch existing default admin user u_1234567890 which will have 20 static + // groups associated with it. Then associate a new ldap account to that + // user + account := ldap.TestAccount(t, testConn, testAuthMethod, "test-login-name", ldap.WithMemberOfGroups(testCtx, testGroupNames...)) + + adminUser, _, err := iamRepo.LookupUser(testCtx, testUser.PublicId) + require.NoError(t, err) + accts, err := iamRepo.AddUserAccounts(testCtx, testUser.PublicId, adminUser.Version, []string{account.PublicId, staticAccount.PublicId}) + require.NoError(t, err) + require.Len(t, accts, 2) + + tests := []struct { + name string + account *ldap.Account + wantMgsCount int + specificMgs []*ldap.ManagedGroup + wantErr errors.Code + wantErrContains string + }{ + { + name: "valid fixed", + account: account, + specificMgs: mgs[0:20], + }, + { + name: "valid fixed, same values", + account: account, + specificMgs: mgs[0:20], + }, + { + name: "valid fixed, new values", + account: account, + specificMgs: mgs[20:40], + }, + { + name: "valid none", + account: account, + wantMgsCount: 0, + }, + { + name: "valid none, second test, testing gracefully aborting", + account: account, + wantMgsCount: 0, + }, + { + name: "valid fixed, prep for random", + account: account, + specificMgs: mgs[20:50], + }, + { + name: "valid random", + account: account, + wantMgsCount: 30, + }, + { + name: "valid random, second test", + account: account, + wantMgsCount: 20, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + // We are intentionally carrying things over between tests to be + // more realistic but that means we need correct versions, so update + // them first. + currMgs, err := repo.ListManagedGroups(testCtx, testAuthMethod.PublicId) + require.NoError(err) + require.Len(currMgs, 101) + currVersionMap := make(map[string]uint32, len(currMgs)) + for _, currMg := range currMgs { + currVersionMap[currMg.PublicId] = currMg.Version + } + for _, mg := range mgs { + mg.Version = currVersionMap[mg.PublicId] + } + + var mgsToTest []*ldap.ManagedGroup + var finalMgs map[string]*ldap.ManagedGroup + + mgsToTest = tc.specificMgs + if mgsToTest == nil { + // Select at random + mgsToTest = make([]*ldap.ManagedGroup, tc.wantMgsCount) + for i := 0; i < tc.wantMgsCount; i++ { + mg := mgs[rand.Int()%len(mgs)] + mgsToTest[i] = mg + } + } + finalMgs = make(map[string]*ldap.ManagedGroup) + for _, v := range mgsToTest { + v.GroupNames = string(testGroupNamesEncodes) + v, _, err = repo.UpdateManagedGroup(testCtx, "global", v, v.Version, []string{"GroupNames"}) + require.NoError(err) + finalMgs[v.PublicId] = v + } + + // Ensure the same set was found; all memberships found should have + // been in the finalMgs map, and when they are all removed there + // should be nothing left. + for _, mship := range mgsToTest { + // Randomly check a few to ensure the MembershipsByGroup function works + members, err := repo.ListManagedGroupMembershipsByGroup(testCtx, mship.PublicId) + require.NoError(err) + require.NotEmpty(members) + var found bool + for _, v := range members { + if v.MemberId == tc.account.GetPublicId() { + found = true + break + } + } + assert.True(found) + delete(finalMgs, mship.PublicId) + } + assert.Len(finalMgs, 0) + + // Now check that the static account still has the same memberships + memberships, err := repo.ListManagedGroupMembershipsByMember(testCtx, staticAccount.PublicId) + require.NoError(err) + assert.Len(memberships, staticMembershipCount) + assert.Equal(memberships[0].ManagedGroupId, staticGroup.PublicId) + }) + } + t.Run("ListManagedGroupMembershipsByGroup-invalid-parameters", func(t *testing.T) { + got, err := repo.ListManagedGroupMembershipsByGroup(testCtx, "") + assert.Error(t, err) + assert.Contains(t, err.Error(), "missing managed group id") + assert.Nil(t, got) + }) + t.Run("ListManagedGroupMembershipsByMember-invalid-parameter", func(t *testing.T) { + got, err := repo.ListManagedGroupMembershipsByMember(testCtx, "") + assert.Error(t, err) + assert.Contains(t, err.Error(), "missing account id") + assert.Nil(t, got) + }) +} + +func TestManagedGroupMemberAccount_SetTableName(t *testing.T) { + t.Parallel() + allocFn := func() *ldap.ManagedGroupMemberAccount { + return &ldap.ManagedGroupMemberAccount{ + ManagedGroupMemberAccount: &store.ManagedGroupMemberAccount{}, + } + } + defaultTableName := "auth_ldap_managed_group_member_account" + tests := []struct { + name string + setNameTo string + want string + }{ + { + name: "new-name", + setNameTo: "new-name", + want: "new-name", + }, + { + name: "reset to default", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + def := allocFn() + require.Equal(defaultTableName, def.TableName()) + m := allocFn() + m.SetTableName(tc.setNameTo) + assert.Equal(tc.want, m.TableName()) + }) + } +} diff --git a/internal/auth/ldap/repository_managed_group_test.go b/internal/auth/ldap/repository_managed_group_test.go new file mode 100644 index 0000000000..6851e2426e --- /dev/null +++ b/internal/auth/ldap/repository_managed_group_test.go @@ -0,0 +1,1227 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "fmt" + "sort" + "strings" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/auth" + "github.com/hashicorp/boundary/internal/auth/ldap/store" + "github.com/hashicorp/boundary/internal/db" + dbassert "github.com/hashicorp/boundary/internal/db/assert" + "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" +) + +func TestRepository_CreateManagedGroup(t *testing.T) { + t.Parallel() + testConn, _ := db.TestSetup(t, "postgres") + testRw := db.New(testConn) + testRootWrapper := db.TestWrapper(t) + + testKms := kms.TestKms(t, testConn, testRootWrapper) + iamRepo := iam.TestRepo(t, testConn, testRootWrapper) + org, _ := iam.TestScopes(t, iamRepo) + + testCtx := context.Background() + orgDbWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + testAuthMethod := TestAuthMethod(t, testConn, orgDbWrapper, org.GetPublicId(), []string{"ldaps://ldap1"}) + + testRepo, err := NewRepository(testCtx, testRw, testRw, testKms) + assert.NoError(t, err) + require.NotNil(t, testRepo) + + acct1 := TestAccount(t, testConn, testAuthMethod, "alice", WithMemberOfGroups(testCtx, testGrpNames...)) + acct2 := TestAccount(t, testConn, testAuthMethod, "eve", WithMemberOfGroups(testCtx, testGrpNames...)) + const notTestGroupName = "not-test-group-name" + acct3 := TestAccount(t, testConn, testAuthMethod, "bob", WithMemberOfGroups(testCtx, notTestGroupName)) + + tests := []struct { + name string + ctx context.Context + repo *Repository + scopeId string + in *ManagedGroup + opts []Option + want *ManagedGroup + wantMgmAcctIds []string + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "nil-ManagedGroup", + ctx: testCtx, + repo: testRepo, + scopeId: org.GetPublicId(), + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing managed group: parameter violation: error #100", + }, + { + name: "nil-embedded-ManagedGroup", + ctx: testCtx, + repo: testRepo, + scopeId: org.GetPublicId(), + in: &ManagedGroup{}, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing embedded managed group: parameter violation: error #100", + }, + { + name: "invalid-no-auth-method-id", + ctx: testCtx, + repo: testRepo, + scopeId: org.GetPublicId(), + in: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{}, + }, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing auth method id: parameter violation: error #100", + }, + { + name: "invalid-no-group-names", + ctx: testCtx, + repo: testRepo, + scopeId: org.GetPublicId(), + in: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + AuthMethodId: testAuthMethod.PublicId, + }, + }, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing group names: parameter violation: error #100", + }, + { + name: "invalid-public-id-set", + ctx: testCtx, + repo: testRepo, + scopeId: org.GetPublicId(), + in: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + AuthMethodId: testAuthMethod.PublicId, + GroupNames: string(TestEncodedGrpNames(t, testGrpNames...)), + PublicId: "mgldap_OOOOOOOOOO", + }, + }, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "public id must be empty: parameter violation: error #100", + }, + { + name: "no-scope", + ctx: testCtx, + repo: testRepo, + in: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + AuthMethodId: testAuthMethod.PublicId, + GroupNames: string(TestEncodedGrpNames(t, testGrpNames...)), + }, + }, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing scope id: parameter violation: error #100", + }, + { + name: "valid-no-options", + ctx: testCtx, + repo: testRepo, + scopeId: org.GetPublicId(), + in: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + AuthMethodId: testAuthMethod.PublicId, + GroupNames: string(TestEncodedGrpNames(t, testGrpNames...)), + }, + }, + want: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + AuthMethodId: testAuthMethod.PublicId, + GroupNames: string(TestEncodedGrpNames(t, testGrpNames...)), + }, + }, + wantMgmAcctIds: []string{acct1.PublicId, acct2.PublicId}, + }, + { + name: "valid-with-name", + ctx: testCtx, + repo: testRepo, + scopeId: org.GetPublicId(), + in: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + AuthMethodId: testAuthMethod.PublicId, + GroupNames: string(TestEncodedGrpNames(t, testGrpNames...)), + Name: "test-name-repo", + }, + }, + want: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + AuthMethodId: testAuthMethod.PublicId, + GroupNames: string(TestEncodedGrpNames(t, testGrpNames...)), + Name: "test-name-repo", + }, + }, + wantMgmAcctIds: []string{acct1.PublicId, acct2.PublicId}, + }, + { + name: "valid-with-description", + ctx: testCtx, + repo: testRepo, + scopeId: org.GetPublicId(), + in: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + AuthMethodId: testAuthMethod.PublicId, + GroupNames: string(TestEncodedGrpNames(t, testGrpNames...)), + Description: ("test-description-repo"), + Name: "myname", + }, + }, + want: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + AuthMethodId: testAuthMethod.PublicId, + GroupNames: string(TestEncodedGrpNames(t, testGrpNames...)), + Description: ("test-description-repo"), + Name: "myname", + }, + }, + wantMgmAcctIds: []string{acct1.PublicId, acct2.PublicId}, + }, + { + name: "not-test-grp-name", + ctx: testCtx, + repo: testRepo, + scopeId: org.GetPublicId(), + in: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + AuthMethodId: testAuthMethod.PublicId, + GroupNames: string(TestEncodedGrpNames(t, notTestGroupName)), + }, + }, + want: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + AuthMethodId: testAuthMethod.PublicId, + GroupNames: string(TestEncodedGrpNames(t, notTestGroupName)), + }, + }, + wantMgmAcctIds: []string{acct3.PublicId}, + }, + { + name: "duplicate-name", // must follow "valid-description" test + ctx: testCtx, + repo: testRepo, + scopeId: org.GetPublicId(), + in: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + AuthMethodId: testAuthMethod.PublicId, + GroupNames: string(TestEncodedGrpNames(t, testGrpNames...)), + Description: ("test-description-repo"), + Name: "myname", + }, + }, + wantErrMatch: errors.T(errors.NotUnique), + wantErrContains: `name "myname" already exists`, + }, + { + name: "get-oplog-wrapper-err", + repo: func() *Repository { + testKms := &mockGetWrapperer{ + getErr: errors.New(testCtx, errors.Encrypt, "test", "get-oplog-wrapper-err"), + } + + testRepo, err := NewRepository(testCtx, testRw, testRw, testKms) + assert.NoError(t, err) + require.NotNil(t, testRepo) + return testRepo + }(), + scopeId: org.GetPublicId(), + in: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + AuthMethodId: testAuthMethod.PublicId, + GroupNames: string(TestEncodedGrpNames(t, testGrpNames...)), + }, + }, + wantErrMatch: errors.T(errors.Encrypt), + wantErrContains: "unable to get oplog wrapper", + }, + { + name: "write-err", + repo: func() *Repository { + conn, mock := db.TestSetupWithMock(t) + mock.ExpectBegin() + mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "version"}).AddRow("1", 1)) // oplog: get ticket + mock.ExpectQuery(`INSERT`).WillReturnError(fmt.Errorf("write-err")) + mock.ExpectRollback() + rw := db.New(conn) + r, err := NewRepository(testCtx, rw, rw, testKms) + require.NoError(t, err) + return r + }(), + scopeId: org.GetPublicId(), + in: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + AuthMethodId: testAuthMethod.PublicId, + GroupNames: string(TestEncodedGrpNames(t, testGrpNames...)), + }, + }, + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "write-err", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := tc.repo.CreateManagedGroup(tc.ctx, tc.scopeId, tc.in, tc.opts...) + if tc.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tc.wantErrMatch, err), "Unexpected error %s", err) + if tc.wantErrContains != "" { + assert.True(strings.Contains(err.Error(), tc.wantErrContains)) + } + return + } + require.NoError(err) + assert.Empty(tc.in.PublicId) + require.NotNil(got) + assertPublicId(t, globals.LdapManagedGroupPrefix, got.PublicId) + assert.NotSame(tc.in, got) + assert.Equal(tc.want.Name, got.Name) + assert.Equal(tc.want.Description, got.Description) + assert.Equal(got.CreateTime, got.UpdateTime) + assert.Equal(tc.want.GroupNames, got.GroupNames) + assert.NoError(db.TestVerifyOplog(t, testRw, got.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second))) + + if tc.wantMgmAcctIds != nil { + mgmAccts := auth.TestManagedGroupMemberAccounts(t, testConn, got.PublicId) + for _, m := range mgmAccts { + t.Log("ManagedGroupId: ", m.ManagedGroupId) + t.Log(" MemberId: ", m.MemberId) + } + wantAccts := make([]*auth.ManagedGroupMemberAccount, 0, len(tc.wantMgmAcctIds)) + for _, id := range tc.wantMgmAcctIds { + wantAccts = append(wantAccts, &auth.ManagedGroupMemberAccount{ + ManagedGroupId: got.PublicId, + MemberId: id, + }) + } + for _, m := range mgmAccts { + m.CreateTime = nil + } + auth.TestSortManagedGroupMemberAccounts(t, wantAccts) + assert.Equal(wantAccts, mgmAccts) + } + }) + } +} + +func TestRepository_LookupManagedGroup(t *testing.T) { + t.Parallel() + testConn, _ := db.TestSetup(t, "postgres") + testRw := db.New(testConn) + rootWrapper := db.TestWrapper(t) + + testKms := kms.TestKms(t, testConn, rootWrapper) + iamRepo := iam.TestRepo(t, testConn, rootWrapper) + org, _ := iam.TestScopes(t, iamRepo) + + testCtx := context.Background() + orgDbWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + authMethod := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId, []string{"ldaps://ldap1"}) + mg := TestManagedGroup(t, testConn, authMethod, testGrpNames) + acct1 := TestAccount(t, testConn, authMethod, "alice", WithMemberOfGroups(testCtx, testGrpNames...)) + acct2 := TestAccount(t, testConn, authMethod, "eve", WithMemberOfGroups(testCtx, testGrpNames...)) + + newMgId, err := newManagedGroupId(testCtx) + require.NoError(t, err) + + testRepo, err := NewRepository(testCtx, testRw, testRw, testKms) + assert.NoError(t, err) + require.NotNil(t, testRepo) + + tests := []struct { + name string + ctx context.Context + repo *Repository + in string + want *ManagedGroup + wantMgmAcct []*auth.ManagedGroupMemberAccount + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "With no public id", + ctx: testCtx, + repo: testRepo, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing public id: parameter violation", + }, + { + name: "With non existing mg id", + ctx: testCtx, + repo: testRepo, + in: newMgId, + }, + { + name: "With existing mg id", + ctx: testCtx, + repo: testRepo, + in: mg.GetPublicId(), + want: mg, + wantMgmAcct: []*auth.ManagedGroupMemberAccount{ + { + CreateTime: mg.CreateTime, + ManagedGroupId: mg.PublicId, + MemberId: acct1.PublicId, + }, + { + CreateTime: mg.CreateTime, + ManagedGroupId: mg.PublicId, + MemberId: acct2.PublicId, + }, + }, + }, + { + name: "read-err", + repo: func() *Repository { + conn, mock := db.TestSetupWithMock(t) + mock.ExpectQuery(`SELECT`).WillReturnError(fmt.Errorf("read-err")) + rw := db.New(conn) + r, err := NewRepository(testCtx, rw, rw, testKms) + require.NoError(t, err) + return r + }(), + in: mg.GetPublicId(), + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "read-err", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := tc.repo.LookupManagedGroup(tc.ctx, tc.in) + if tc.wantErrMatch != nil { + assert.Truef(errors.Match(tc.wantErrMatch, err), "Unexpected error %s", err) + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.EqualValues(tc.want, got) + if tc.wantMgmAcct != nil { + mgmAccts := auth.TestManagedGroupMemberAccounts(t, testConn, tc.want.PublicId) + for _, m := range mgmAccts { + t.Log("ManagedGroupId: ", m.ManagedGroupId) + t.Log(" MemberId: ", m.MemberId) + } + auth.TestSortManagedGroupMemberAccounts(t, tc.wantMgmAcct) + assert.Equal(tc.wantMgmAcct, mgmAccts) + } + }) + } +} + +func TestRepository_DeleteManagedGroup(t *testing.T) { + t.Parallel() + testConn, _ := db.TestSetup(t, "postgres") + testRw := db.New(testConn) + testWrapper := db.TestWrapper(t) + + testCtx := context.Background() + testKms := kms.TestKms(t, testConn, testWrapper) + iamRepo := iam.TestRepo(t, testConn, testWrapper) + org, _ := iam.TestScopes(t, iamRepo) + + orgDbWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + authMethod := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId, []string{"ldaps://ldap1"}) + + mg := TestManagedGroup(t, testConn, authMethod, testGrpNames) + newMgId, err := newManagedGroupId(testCtx) + require.NoError(t, err) + + testRepo, err := NewRepository(testCtx, testRw, testRw, testKms) + assert.NoError(t, err) + require.NotNil(t, testRepo) + + tests := []struct { + name string + ctx context.Context + repo *Repository + scopeId string + in string + want int + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "With no scope id", + ctx: testCtx, + repo: testRepo, + scopeId: "", + in: mg.GetPublicId(), + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing scope id: parameter violation: error #100", + }, + { + name: "With no public id", + ctx: testCtx, + repo: testRepo, + scopeId: org.GetPublicId(), + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing public id: parameter violation: error #100", + }, + { + name: "With non existing managed group id", + ctx: testCtx, + repo: testRepo, + scopeId: org.GetPublicId(), + in: newMgId, + wantErrMatch: errors.T(errors.RecordNotFound), + wantErrContains: "managed group not found", + }, + { + name: "get-oplog-wrapper-err", + repo: func() *Repository { + testKms := &mockGetWrapperer{ + getErr: errors.New(testCtx, errors.Encrypt, "test", "get-oplog-wrapper-err"), + } + + testRepo, err := NewRepository(testCtx, testRw, testRw, testKms) + assert.NoError(t, err) + require.NotNil(t, testRepo) + return testRepo + }(), + scopeId: org.GetPublicId(), + in: mg.GetPublicId(), + wantErrMatch: errors.T(errors.Encrypt), + wantErrContains: "unable to get oplog wrapper", + }, + { + name: "too-many-rows-affected", + repo: func() *Repository { + conn, mock := db.TestSetupWithMock(t) + mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "scope_id", "auth_method_id"}).AddRow("1", "global", "1")) // get account + mock.ExpectBegin() + mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "version"}).AddRow("1", 1)) // oplog: get ticket + mock.ExpectExec(`DELETE`).WillReturnResult(sqlmock.NewResult(1, 2)) + mock.ExpectQuery(`INSERT`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("1")) // oplog: entry + mock.ExpectQuery(`INSERT`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("1")) // oplog: metadata + mock.ExpectExec(`UPDATE`).WillReturnResult(sqlmock.NewResult(0, 1)) // oplog: update ticket version + mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "version"}).AddRow("1", 1)) // oplog: get ticket + + mock.ExpectRollback() + rw := db.New(conn) + r, err := NewRepository(testCtx, rw, rw, testKms) + require.NoError(t, err) + return r + }(), + scopeId: org.GetPublicId(), + in: mg.GetPublicId(), + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "more than 1 resource would have been deleted", + }, + { + name: "delete-err", + repo: func() *Repository { + conn, mock := db.TestSetupWithMock(t) + mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "scope_id", "auth_method_id"}).AddRow("1", "global", "1")) // get account + mock.ExpectBegin() + mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "version"}).AddRow("1", 1)) // oplog: get ticket + mock.ExpectExec(`DELETE`).WillReturnError(fmt.Errorf("delete-err")) + mock.ExpectRollback() + rw := db.New(conn) + r, err := NewRepository(testCtx, rw, rw, testKms) + require.NoError(t, err) + return r + }(), + scopeId: org.GetPublicId(), + in: mg.GetPublicId(), + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "delete-err", + }, + { + name: "oplog-metadata-err", + repo: func() *Repository { + conn, mock := db.TestSetupWithMock(t) + mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "scope_id"}).AddRow("1", "global")) // get account without auth method id + rw := db.New(conn) + r, err := NewRepository(testCtx, rw, rw, testKms) + require.NoError(t, err) + return r + }(), + scopeId: org.GetPublicId(), + in: mg.GetPublicId(), + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "unable to generate oplog metadata", + }, + { + name: "With existing managed group id", + ctx: testCtx, + repo: testRepo, + scopeId: org.GetPublicId(), + in: mg.GetPublicId(), + want: 1, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := tc.repo.DeleteManagedGroup(context.Background(), tc.scopeId, tc.in) + if tc.wantErrMatch != nil { + assert.Truef(errors.Match(tc.wantErrMatch, err), "Unexpected error %s", err) + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.EqualValues(tc.want, got) + }) + } +} + +func TestRepository_ListManagedGroups(t *testing.T) { + t.Parallel() + testConn, _ := db.TestSetup(t, "postgres") + testRw := db.New(testConn) + testRootWrapper := db.TestWrapper(t) + + testCtx := context.Background() + testKms := kms.TestKms(t, testConn, testRootWrapper) + iamRepo := iam.TestRepo(t, testConn, testRootWrapper) + org, _ := iam.TestScopes(t, iamRepo) + + orgDbWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + authMethod1 := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId, []string{"ldaps://ldap1"}) + authMethod2 := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId, []string{"ldaps://ldap2"}) + authMethod3 := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId, []string{"ldaps://ldap3"}) + + mgs1 := []*ManagedGroup{ + TestManagedGroup(t, testConn, authMethod1, testGrpNames), + TestManagedGroup(t, testConn, authMethod1, testGrpNames), + TestManagedGroup(t, testConn, authMethod1, testGrpNames), + } + sort.Slice(mgs1, func(i, j int) bool { + return strings.Compare(mgs1[i].PublicId, mgs1[j].PublicId) < 0 + }) + + mgs2 := []*ManagedGroup{ + TestManagedGroup(t, testConn, authMethod2, testGrpNames), + TestManagedGroup(t, testConn, authMethod2, testGrpNames), + TestManagedGroup(t, testConn, authMethod2, testGrpNames), + } + sort.Slice(mgs2, func(i, j int) bool { + return strings.Compare(mgs2[i].PublicId, mgs2[j].PublicId) < 0 + }) + + testRepo, err := NewRepository(testCtx, testRw, testRw, testKms) + assert.NoError(t, err) + require.NotNil(t, testRepo) + + tests := []struct { + name string + ctx context.Context + repo *Repository + in string + opts []Option + want []*ManagedGroup + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "With no auth method id", + ctx: testCtx, + repo: testRepo, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing auth method id: parameter violation: error #100", + }, + { + name: "With no managed groups", + ctx: testCtx, + repo: testRepo, + in: authMethod3.GetPublicId(), + want: []*ManagedGroup{}, + }, + { + name: "With first auth method id", + ctx: testCtx, + repo: testRepo, + in: authMethod1.GetPublicId(), + want: mgs1, + }, + { + name: "With first auth method id", + ctx: testCtx, + repo: testRepo, + in: authMethod2.GetPublicId(), + want: mgs2, + }, + { + name: "read-err", + repo: func() *Repository { + conn, mock := db.TestSetupWithMock(t) + mock.ExpectQuery(`SELECT`).WillReturnError(fmt.Errorf("read-err")) + rw := db.New(conn) + r, err := NewRepository(testCtx, rw, rw, testKms) + require.NoError(t, err) + return r + }(), + in: authMethod1.GetPublicId(), + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "read-err", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := tc.repo.ListManagedGroups(tc.ctx, tc.in, tc.opts...) + if tc.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tc.wantErrMatch, err), "Unexpected error %s", err) + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + + sort.Slice(got, func(i, j int) bool { + return strings.Compare(got[i].PublicId, got[j].PublicId) < 0 + }) + assert.EqualValues(tc.want, got) + }) + } +} + +func TestRepository_ListManagedGroups_Limits(t *testing.T) { + t.Parallel() + testConn, _ := db.TestSetup(t, "postgres") + testRw := db.New(testConn) + testRootWrapper := db.TestWrapper(t) + + testCtx := context.Background() + testKms := kms.TestKms(t, testConn, testRootWrapper) + iamRepo := iam.TestRepo(t, testConn, testRootWrapper) + org, _ := iam.TestScopes(t, iamRepo) + + orgDbWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + am := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId, []string{"ldaps://ldap1"}) + mgCount := 10 + for i := 0; i < mgCount; i++ { + TestManagedGroup(t, testConn, am, testGrpNames) + } + + tests := []struct { + name string + repoOpts []Option + listOpts []Option + wantLen int + }{ + { + name: "With no limits", + wantLen: mgCount, + }, + { + name: "With repo limit", + repoOpts: []Option{WithLimit(testCtx, 3)}, + wantLen: 3, + }, + { + name: "With negative repo limit", + repoOpts: []Option{WithLimit(testCtx, -1)}, + wantLen: mgCount, + }, + { + name: "With List limit", + listOpts: []Option{WithLimit(testCtx, 3)}, + wantLen: 3, + }, + { + name: "With negative List limit", + listOpts: []Option{WithLimit(testCtx, -1)}, + wantLen: mgCount, + }, + { + name: "With repo smaller than list limit", + repoOpts: []Option{WithLimit(testCtx, 2)}, + listOpts: []Option{WithLimit(testCtx, 6)}, + wantLen: 6, + }, + { + name: "With repo larger than list limit", + repoOpts: []Option{WithLimit(testCtx, 6)}, + listOpts: []Option{WithLimit(testCtx, 2)}, + wantLen: 2, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(testCtx, testRw, testRw, testKms, tc.repoOpts...) + assert.NoError(err) + require.NotNil(repo) + got, err := repo.ListManagedGroups(context.Background(), am.GetPublicId(), tc.listOpts...) + require.NoError(err) + assert.Len(got, tc.wantLen) + }) + } +} + +func TestRepository_UpdateManagedGroup(t *testing.T) { + t.Parallel() + testCtx := context.Background() + testConn, _ := db.TestSetup(t, "postgres") + testRw := db.New(testConn) + testRootWrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, testConn, testRootWrapper) + iamRepo := iam.TestRepo(t, testConn, testRootWrapper) + org, _ := iam.TestScopes(t, iamRepo) + + orgDbWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + am := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId, []string{"ldaps://ldap1"}) + + testRepo, err := NewRepository(testCtx, testRw, testRw, testKms) + assert.NoError(t, err) + require.NotNil(t, testRepo) + + changeName := func(s string) func(*ManagedGroup) *ManagedGroup { + return func(mg *ManagedGroup) *ManagedGroup { + mg.Name = s + return mg + } + } + + changeDescription := func(s string) func(*ManagedGroup) *ManagedGroup { + return func(mg *ManagedGroup) *ManagedGroup { + mg.Description = s + return mg + } + } + + changeGrpNames := func(s string) func(*ManagedGroup) *ManagedGroup { + return func(mg *ManagedGroup) *ManagedGroup { + mg.GroupNames = s + return mg + } + } + + makeNil := func() func(*ManagedGroup) *ManagedGroup { + return func(mg *ManagedGroup) *ManagedGroup { + return nil + } + } + + makeEmbeddedNil := func() func(*ManagedGroup) *ManagedGroup { + return func(mg *ManagedGroup) *ManagedGroup { + return &ManagedGroup{} + } + } + + deletePublicId := func() func(*ManagedGroup) *ManagedGroup { + return func(mg *ManagedGroup) *ManagedGroup { + mg.PublicId = "" + return mg + } + } + + nonExistentPublicId := func() func(*ManagedGroup) *ManagedGroup { + return func(mg *ManagedGroup) *ManagedGroup { + mg.PublicId = "abcd_OOOOOOOOOO" + return mg + } + } + + combine := func(fns ...func(mg *ManagedGroup) *ManagedGroup) func(*ManagedGroup) *ManagedGroup { + return func(mg *ManagedGroup) *ManagedGroup { + for _, fn := range fns { + mg = fn(mg) + } + return mg + } + } + + tests := []struct { + name string + repo *Repository + scopeId string + version uint32 + orig *ManagedGroup + chgFn func(*ManagedGroup) *ManagedGroup + masks []string + want *ManagedGroup + wantCount int + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "nil-ManagedGroup", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{}, + }, + chgFn: makeNil(), + masks: []string{NameField, DescriptionField}, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing ManagedGroup: parameter violation: error #100", + }, + { + name: "nil-embedded-ManagedGroup", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{}, + }, + chgFn: makeEmbeddedNil(), + masks: []string{NameField, DescriptionField}, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing embedded ManagedGroup: parameter violation: error #100", + }, + { + name: "no-scope-id", + repo: testRepo, + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "no-scope-id-test-name-repo", + }, + }, + chgFn: changeName("no-scope-id-test-update-name-repo"), + masks: []string{NameField}, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing scope id: parameter violation: error #100", + }, + { + name: "missing-version", + repo: testRepo, + scopeId: org.GetPublicId(), + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "missing-version-test-name-repo", + }, + }, + chgFn: changeName("test-update-name-repo"), + masks: []string{NameField}, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing version: parameter violation: error #100", + }, + { + name: "no-public-id", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{}, + }, + chgFn: deletePublicId(), + masks: []string{NameField, DescriptionField}, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing public id: parameter violation: error #100", + }, + { + name: "updating-non-existent-ManagedGroup", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "updating-non-existent-ManagedGroup-test-name-repo", + }, + }, + chgFn: combine(nonExistentPublicId(), changeName("updating-non-existent-ManagedGroup-test-update-name-repo")), + masks: []string{NameField}, + wantErrMatch: errors.T(errors.RecordNotFound), + wantErrContains: "record not found, search issue: error #1100", + }, + { + name: "empty-field-mask", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "empty-field-mask-test-name-repo", + }, + }, + chgFn: changeName("empty-field-mask-test-update-name-repo"), + wantErrMatch: errors.T(errors.EmptyFieldMask), + wantErrContains: "missing field mask: parameter violation: error #104", + }, + { + name: "read-only-fields-in-field-mask", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "read-only-fields-in-field-mask-test-name-repo", + }, + }, + chgFn: changeName("read-only-fields-in-field-mask-test-update-name-repo"), + masks: []string{"PublicId", "CreateTime", "UpdateTime", "AuthMethodId"}, + wantErrMatch: errors.T(errors.InvalidFieldMask), + wantErrContains: "PublicId: parameter violation: error #103", + }, + { + name: "unknown-field-in-field-mask", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "unknown-field-in-field-mask-test-name-repo", + }, + }, + chgFn: changeName("unknown-field-in-field-mask-test-update-name-repo"), + masks: []string{"Bilbo"}, + wantErrMatch: errors.T(errors.InvalidFieldMask), + wantErrContains: "Bilbo: parameter violation: error #103", + }, + { + name: "change-name", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "change-name-test-name-repo", + }, + }, + chgFn: changeName("change-name-test-update-name-repo"), + masks: []string{NameField}, + want: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "change-name-test-update-name-repo", + }, + }, + wantCount: 1, + }, + { + name: "change-description", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Description: "test-description-repo", + }, + }, + chgFn: changeDescription("test-update-description-repo"), + masks: []string{DescriptionField}, + want: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Description: "test-update-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "change-grp-names", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + GroupNames: TestEncodedGrpNames(t, "orig-admin", "orig-users"), + }, + }, + chgFn: changeGrpNames(TestEncodedGrpNames(t, testGrpNames...)), + masks: []string{GroupNamesField}, + want: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + GroupNames: TestEncodedGrpNames(t, testGrpNames...), + }, + }, + wantCount: 1, + }, + { + name: "change-name-and-description", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "change-name-and-description-test-name-repo", + Description: "test-description-repo", + }, + }, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("change-name-and-description-test-update-name-repo")), + masks: []string{NameField, DescriptionField}, + want: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "change-name-and-description-test-update-name-repo", + Description: "test-update-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "delete-name", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "delete-name-test-name-repo", + Description: "test-description-repo", + }, + }, + masks: []string{NameField}, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("")), + want: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Description: "test-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "delete-description", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "delete-description-test-name-repo", + Description: "test-description-repo", + }, + }, + masks: []string{DescriptionField}, + chgFn: combine(changeDescription(""), changeName("test-update-name-repo")), + want: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "delete-description-test-name-repo", + }, + }, + wantCount: 1, + }, + { + name: "delete-grp-names", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + GroupNames: TestEncodedGrpNames(t, testGrpNames...), + }, + }, + masks: []string{GroupNamesField}, + chgFn: combine(changeGrpNames("")), + wantErrMatch: errors.T(errors.NotNull), + wantErrContains: "group_names must not be empty: not null constraint violated: integrity violation: error #1001", + }, + { + name: "do-not-delete-name", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "do-not-delete-name-test-name-repo", + Description: "test-description-repo", + }, + }, + masks: []string{DescriptionField}, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("")), + want: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "do-not-delete-name-test-name-repo", + Description: "test-update-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "do-not-delete-description", + repo: testRepo, + scopeId: org.GetPublicId(), + version: 1, + orig: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "do-not-delete-description-test-name-repo", + Description: "test-description-repo", + }, + }, + masks: []string{NameField}, + chgFn: combine(changeDescription(""), changeName("do-not-delete-description-test-update-name-repo")), + want: &ManagedGroup{ + ManagedGroup: &store.ManagedGroup{ + Name: "do-not-delete-description-test-update-name-repo", + Description: "test-description-repo", + }, + }, + wantCount: 1, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + require.NotEmpty(tc.repo) + + orig := TestManagedGroup(t, testConn, am, testGrpNames, WithName(testCtx, tc.orig.GetName()), WithDescription(testCtx, tc.orig.GetDescription())) + + tc.orig.AuthMethodId = am.PublicId + if tc.chgFn != nil { + orig = tc.chgFn(orig) + } + got, gotCount, err := tc.repo.UpdateManagedGroup(testCtx, tc.scopeId, orig, tc.version, tc.masks) + if tc.wantErrMatch != nil { + assert.True(errors.Match(tc.wantErrMatch, err), "want err: %q got: %q", tc.wantErrMatch.Code, err) + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + assert.Equal(tc.wantCount, gotCount, "row count") + assert.Nil(got) + return + } + assert.NoError(err) + assert.Empty(tc.orig.PublicId) + if tc.wantCount == 0 { + assert.Equal(tc.wantCount, gotCount, "row count") + assert.Nil(got) + return + } + require.NotNil(got) + assertPublicId(t, globals.LdapManagedGroupPrefix, got.PublicId) + assert.Equal(tc.wantCount, gotCount, "row count") + assert.NotSame(tc.orig, got) + assert.Equal(tc.orig.AuthMethodId, got.AuthMethodId) + underlyingDB, err := testConn.SqlDB(testCtx) + require.NoError(err) + dbassert := dbassert.New(t, underlyingDB) + if tc.want.Name == "" { + dbassert.IsNull(got, "name") + return + } + assert.Equal(tc.want.Name, got.Name) + if tc.want.Description == "" { + dbassert.IsNull(got, "description") + return + } + assert.Equal(tc.want.Description, got.Description) + if tc.wantCount > 0 { + assert.NoError(db.TestVerifyOplog(t, testRw, got.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) + } + }) + } +} diff --git a/internal/auth/ldap/repository_test.go b/internal/auth/ldap/repository_test.go new file mode 100644 index 0000000000..9a14ed4fb3 --- /dev/null +++ b/internal/auth/ldap/repository_test.go @@ -0,0 +1,121 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewRepository(t *testing.T) { + ctx := context.TODO() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + + type args struct { + r db.Reader + w db.Writer + kms *kms.Kms + opts []Option + } + tests := []struct { + name string + args args + want *Repository + wantErrMatch *errors.Template + }{ + { + name: "valid", + args: args{ + r: rw, + w: rw, + kms: kmsCache, + }, + want: &Repository{ + reader: rw, + writer: rw, + kms: kmsCache, + defaultLimit: db.DefaultLimit, + }, + }, + { + name: "valid with limit", + args: args{ + r: rw, + w: rw, + kms: kmsCache, + opts: []Option{WithLimit(context.Background(), 5)}, + }, + want: &Repository{ + reader: rw, + writer: rw, + kms: kmsCache, + defaultLimit: 5, + }, + }, + { + name: "nil-reader", + args: args{ + r: nil, + w: rw, + kms: kmsCache, + }, + want: nil, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "nil-writer", + args: args{ + r: rw, + w: nil, + kms: kmsCache, + }, + want: nil, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "nil-wrapper", + args: args{ + r: rw, + w: rw, + kms: nil, + }, + want: nil, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "all-nils", + args: args{ + r: nil, + w: nil, + kms: nil, + }, + want: nil, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := NewRepository(ctx, tt.args.r, tt.args.w, tt.args.kms, tt.args.opts...) + if tt.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tt.wantErrMatch, err), "want err code: %q got: %q", tt.wantErrMatch, err) + assert.Nil(got) + return + } + require.NoError(err) + require.NotNil(got) + assert.Equal(tt.want, got) + }) + } +} diff --git a/internal/auth/ldap/rewrapping.go b/internal/auth/ldap/rewrapping.go new file mode 100644 index 0000000000..a9dc39d075 --- /dev/null +++ b/internal/auth/ldap/rewrapping.go @@ -0,0 +1,118 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/libs/crypto" + "github.com/hashicorp/boundary/internal/util" + wrapping "github.com/hashicorp/go-kms-wrapping/v2" +) + +const ( + CtCertificateKeyField = "CtCertificateKey" + CtPasswordField = "CtPassword" + KeyIdField = "KeyId" +) + +func init() { + kms.RegisterTableRewrapFn(clientCertificateTableName, clientCertificateRewrapFn) + kms.RegisterTableRewrapFn(bindCredentialTableName, bindCredentialRewrapFn) +} + +// hmacField simply hmac's a field in a consistent manner for this pkg +func hmacField(ctx context.Context, cipher wrapping.Wrapper, field []byte, publicId string) ([]byte, error) { + const op = "ldap.hmacField" + hm, err := crypto.HmacSha256(ctx, field, cipher, []byte(publicId), nil) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + return []byte(hm), nil +} + +// bindCredentialRewrapFn provides a kms.Rewrapfn for the BindCredential type +func bindCredentialRewrapFn(ctx context.Context, dataKeyVersionId, scopeId string, reader db.Reader, writer db.Writer, kmsRepo kms.GetWrapperer) error { + const op = "ldap.bindCredentialRewrapFn" + if dataKeyVersionId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing data key version id") + } + if scopeId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing scope id") + } + if util.IsNil(reader) { + return errors.New(ctx, errors.InvalidParameter, op, "missing database reader") + } + if util.IsNil(writer) { + return errors.New(ctx, errors.InvalidParameter, op, "missing database writer") + } + if util.IsNil(kmsRepo) { + return errors.New(ctx, errors.InvalidParameter, op, "missing kms repository") + } + var creds []*BindCredential + // This is the fastest query we can use without creating a new index on key_id. + if err := reader.SearchWhere(ctx, &creds, "key_id=?", []any{dataKeyVersionId}, db.WithLimit(-1)); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query sql for rows that need rewrapping")) + } + wrapper, err := kmsRepo.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to fetch kms wrapper for rewrapping")) + } + for _, cc := range creds { + if err := cc.decrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to decrypt bind credential")) + } + if err := cc.encrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to re-encrypt bind credential")) + } + if _, err := writer.Update(ctx, cc, []string{CtPasswordField, KeyIdField}, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to update bind credential row with rewrapped fields")) + } + } + return nil +} + +// clientCertificateRewrapFn provides a kms.Rewrapfn for the ClientCertificate type +func clientCertificateRewrapFn(ctx context.Context, dataKeyVersionId, scopeId string, reader db.Reader, writer db.Writer, kmsRepo kms.GetWrapperer) error { + const op = "ldap.clientCertificateRewrapFn" + if dataKeyVersionId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing data key version id") + } + if scopeId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing scope id") + } + if util.IsNil(reader) { + return errors.New(ctx, errors.InvalidParameter, op, "missing database reader") + } + if util.IsNil(writer) { + return errors.New(ctx, errors.InvalidParameter, op, "missing database writer") + } + if kmsRepo == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing kms repository") + } + var clientCerts []*ClientCertificate + // This is the fastest query we can use without creating a new index on key_id. + if err := reader.SearchWhere(ctx, &clientCerts, "key_id=?", []any{dataKeyVersionId}, db.WithLimit(-1)); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query sql for rows that need rewrapping")) + } + wrapper, err := kmsRepo.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to fetch kms wrapper for rewrapping")) + } + for _, cc := range clientCerts { + if err := cc.decrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to decrypt client certificate")) + } + if err := cc.encrypt(ctx, wrapper); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to re-encrypt client certificate")) + } + if _, err := writer.Update(ctx, cc, []string{CtCertificateKeyField, KeyIdField}, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to update client certificate row with rewrapped fields")) + } + } + return nil +} diff --git a/internal/auth/ldap/rewrapping_test.go b/internal/auth/ldap/rewrapping_test.go new file mode 100644 index 0000000000..6dc80ddd8b --- /dev/null +++ b/internal/auth/ldap/rewrapping_test.go @@ -0,0 +1,616 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "crypto/ed25519" + "crypto/rand" + "crypto/x509" + "testing" + + "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/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRewrap_bindCredentialRewrapFn(t *testing.T) { + t.Parallel() + testCtx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + testRootWrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, conn, testRootWrapper) + rw := db.New(conn) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, testRootWrapper)) + orgDBWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + tests := []struct { + name string + ctx context.Context + setup func() (string, string, *BindCredential, *AuthMethod) + reader db.Reader + writer db.Writer + kmsRepo kms.GetWrapperer + wantErr bool + wantErrCode errors.Code + wantErrContains string + }{ + { + name: "success", + ctx: testCtx, + setup: func() (string, string, *BindCredential, *AuthMethod) { + am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithBindCredential(testCtx, "bind-dn", "bind-password")) + bc := allocBindCredential() + err = rw.LookupWhere(testCtx, &bc, "ldap_method_id = ?", []any{am.PublicId}) + require.NoError(t, err) + require.NoError(t, bc.decrypt(testCtx, orgDBWrapper)) + assert.NotEmpty(t, bc.GetKeyId()) + assert.NotEmpty(t, bc.GetPassword()) + assert.NotEmpty(t, bc.GetPasswordHmac()) + assert.NotEmpty(t, bc.GetCtPassword()) + return bc.GetKeyId(), am.GetScopeId(), bc, am + }, + reader: rw, + writer: rw, + kmsRepo: testKms, + }, + { + name: "missing-key-id", + ctx: testCtx, + setup: func() (string, string, *BindCredential, *AuthMethod) { + am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithBindCredential(testCtx, "bind-dn", "bind-password")) + bc := allocBindCredential() + err = rw.LookupWhere(testCtx, &bc, "ldap_method_id = ?", []any{am.PublicId}) + require.NoError(t, err) + require.NoError(t, bc.decrypt(testCtx, orgDBWrapper)) + assert.NotEmpty(t, bc.GetKeyId()) + assert.NotEmpty(t, bc.GetPassword()) + assert.NotEmpty(t, bc.GetPasswordHmac()) + assert.NotEmpty(t, bc.GetCtPassword()) + return "", am.GetScopeId(), bc, am + }, + reader: rw, + writer: rw, + kmsRepo: testKms, + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "missing data key version id", + }, + { + name: "missing-scope-id", + ctx: testCtx, + setup: func() (string, string, *BindCredential, *AuthMethod) { + am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithBindCredential(testCtx, "bind-dn", "bind-password")) + bc := allocBindCredential() + err = rw.LookupWhere(testCtx, &bc, "ldap_method_id = ?", []any{am.PublicId}) + require.NoError(t, err) + require.NoError(t, bc.decrypt(testCtx, orgDBWrapper)) + assert.NotEmpty(t, bc.GetKeyId()) + assert.NotEmpty(t, bc.GetPassword()) + assert.NotEmpty(t, bc.GetPasswordHmac()) + assert.NotEmpty(t, bc.GetCtPassword()) + return bc.GetKeyId(), "", bc, am + }, + reader: rw, + writer: rw, + kmsRepo: testKms, + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "missing scope id", + }, + { + name: "missing-reader", + ctx: testCtx, + setup: func() (string, string, *BindCredential, *AuthMethod) { + am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithBindCredential(testCtx, "bind-dn", "bind-password")) + bc := allocBindCredential() + err = rw.LookupWhere(testCtx, &bc, "ldap_method_id = ?", []any{am.PublicId}) + require.NoError(t, err) + require.NoError(t, bc.decrypt(testCtx, orgDBWrapper)) + assert.NotEmpty(t, bc.GetKeyId()) + assert.NotEmpty(t, bc.GetPassword()) + assert.NotEmpty(t, bc.GetPasswordHmac()) + assert.NotEmpty(t, bc.GetCtPassword()) + return bc.GetKeyId(), am.GetScopeId(), bc, am + }, + reader: nil, + writer: rw, + kmsRepo: testKms, + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "missing database reader", + }, + { + name: "missing-writer", + ctx: testCtx, + setup: func() (string, string, *BindCredential, *AuthMethod) { + am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithBindCredential(testCtx, "bind-dn", "bind-password")) + bc := allocBindCredential() + err = rw.LookupWhere(testCtx, &bc, "ldap_method_id = ?", []any{am.PublicId}) + require.NoError(t, err) + require.NoError(t, bc.decrypt(testCtx, orgDBWrapper)) + assert.NotEmpty(t, bc.GetKeyId()) + assert.NotEmpty(t, bc.GetPassword()) + assert.NotEmpty(t, bc.GetPasswordHmac()) + assert.NotEmpty(t, bc.GetCtPassword()) + return bc.GetKeyId(), am.GetScopeId(), bc, am + }, + reader: rw, + writer: nil, + kmsRepo: testKms, + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "missing database writer", + }, + { + name: "missing-kms", + ctx: testCtx, + setup: func() (string, string, *BindCredential, *AuthMethod) { + am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithBindCredential(testCtx, "bind-dn", "bind-password")) + bc := allocBindCredential() + err = rw.LookupWhere(testCtx, &bc, "ldap_method_id = ?", []any{am.PublicId}) + require.NoError(t, err) + require.NoError(t, bc.decrypt(testCtx, orgDBWrapper)) + assert.NotEmpty(t, bc.GetKeyId()) + assert.NotEmpty(t, bc.GetPassword()) + assert.NotEmpty(t, bc.GetPasswordHmac()) + assert.NotEmpty(t, bc.GetCtPassword()) + return bc.GetKeyId(), am.GetScopeId(), bc, am + }, + reader: rw, + writer: rw, + kmsRepo: nil, + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "missing kms repository", + }, + { + name: "GetWrapper-err", + ctx: testCtx, + setup: func() (string, string, *BindCredential, *AuthMethod) { + am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithBindCredential(testCtx, "bind-dn", "bind-password")) + bc := allocBindCredential() + err = rw.LookupWhere(testCtx, &bc, "ldap_method_id = ?", []any{am.PublicId}) + require.NoError(t, err) + require.NoError(t, bc.decrypt(testCtx, orgDBWrapper)) + assert.NotEmpty(t, bc.GetKeyId()) + assert.NotEmpty(t, bc.GetPassword()) + assert.NotEmpty(t, bc.GetPasswordHmac()) + assert.NotEmpty(t, bc.GetCtPassword()) + return bc.GetKeyId(), am.GetScopeId(), bc, am + }, + reader: rw, + writer: rw, + kmsRepo: &kms.MockGetWrapperer{ + GetErr: errors.New(testCtx, errors.Internal, "test", "GetWrapper error"), + }, + wantErr: true, + wantErrCode: errors.Internal, + wantErrContains: "GetWrapper error", + }, + { + name: "encrypt-err", + ctx: testCtx, + setup: func() (string, string, *BindCredential, *AuthMethod) { + am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithBindCredential(testCtx, "bind-dn", "bind-password")) + bc := allocBindCredential() + err = rw.LookupWhere(testCtx, &bc, "ldap_method_id = ?", []any{am.PublicId}) + require.NoError(t, err) + require.NoError(t, bc.decrypt(testCtx, orgDBWrapper)) + assert.NotEmpty(t, bc.GetKeyId()) + assert.NotEmpty(t, bc.GetPassword()) + assert.NotEmpty(t, bc.GetPasswordHmac()) + assert.NotEmpty(t, bc.GetCtPassword()) + return bc.GetKeyId(), am.GetScopeId(), bc, am + }, + reader: rw, + writer: rw, + kmsRepo: &kms.MockGetWrapperer{ + ReturnWrapper: &kms.MockWrapper{ + EncryptErr: errors.New(testCtx, errors.Encrypt, "test", "encrypt error"), + Wrapper: orgDBWrapper, + }, + }, + wantErr: true, + wantErrCode: errors.Encrypt, + wantErrContains: "encrypt error", + }, + { + name: "decrypt-err", + ctx: testCtx, + setup: func() (string, string, *BindCredential, *AuthMethod) { + am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithBindCredential(testCtx, "bind-dn", "bind-password")) + bc := allocBindCredential() + err = rw.LookupWhere(testCtx, &bc, "ldap_method_id = ?", []any{am.PublicId}) + require.NoError(t, err) + require.NoError(t, bc.decrypt(testCtx, orgDBWrapper)) + assert.NotEmpty(t, bc.GetKeyId()) + assert.NotEmpty(t, bc.GetPassword()) + assert.NotEmpty(t, bc.GetPasswordHmac()) + assert.NotEmpty(t, bc.GetCtPassword()) + return bc.GetKeyId(), am.GetScopeId(), bc, am + }, + reader: rw, + writer: rw, + kmsRepo: &kms.MockGetWrapperer{ + ReturnWrapper: &kms.MockWrapper{ + DecryptErr: errors.New(testCtx, errors.Encrypt, "test", "decrypt error"), + Wrapper: orgDBWrapper, + }, + }, + wantErr: true, + wantErrCode: errors.Decrypt, + wantErrContains: "decrypt error", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + keyId, scopeId, bc, am := tc.setup() + + // now we can rotate and rewrap + assert.NoError(testKms.RotateKeys(testCtx, org.Scope.GetPublicId())) + + // let's do this rewrapping! + err := bindCredentialRewrapFn(tc.ctx, keyId, scopeId, tc.reader, tc.writer, tc.kmsRepo) + if tc.wantErr { + require.Error(err) + if tc.wantErrCode != errors.Unknown { + assert.True(errors.Match(errors.T(tc.wantErrCode), err)) + } + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + + // fetch the new key version + kmsWrapper, err := testKms.GetWrapper(testCtx, org.Scope.GetPublicId(), kms.KeyPurposeDatabase) + assert.NoError(err) + newKeyVersion, err := kmsWrapper.KeyId(testCtx) + assert.NoError(err) + + // get the latest bind cred + latestBindCred := allocBindCredential() + err = rw.LookupWhere(testCtx, &latestBindCred, "ldap_method_id = ?", []any{am.GetPublicId()}) + require.NoError(err) + require.NoError(latestBindCred.decrypt(testCtx, kmsWrapper)) + + // make sure the password and its hmac are correct and that it uses + // the newest key version id + assert.NotEmpty(latestBindCred.KeyId) + assert.Equal(newKeyVersion, latestBindCred.KeyId) + assert.Equal([]byte("bind-password"), latestBindCred.Password) + assert.NotEqual(bc.CtPassword, latestBindCred.CtPassword) + assert.NotEmpty(latestBindCred.PasswordHmac) + assert.Equal(bc.PasswordHmac, latestBindCred.PasswordHmac) + }) + } +} + +func TestRewrap_clientCertificateRewrapFn(t *testing.T) { + t.Parallel() + testCtx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + testRootWrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, conn, testRootWrapper) + rw := db.New(conn) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, testRootWrapper)) + orgDBWrapper, err := testKms.GetWrapper(testCtx, org.GetPublicId(), kms.KeyPurposeDatabase) + require.NoError(t, err) + tests := []struct { + name string + ctx context.Context + setup func() (string, string, *ClientCertificate, *AuthMethod) + reader db.Reader + writer db.Writer + kmsRepo kms.GetWrapperer + wantErr bool + wantErrCode errors.Code + wantErrContains string + }{ + { + name: "success", + ctx: testCtx, + setup: func() (string, string, *ClientCertificate, *AuthMethod) { + _, privKey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + cert, _ := TestGenerateCA(t, "localhost") + derPrivKey, err := x509.MarshalPKCS8PrivateKey(privKey) + require.NoError(t, err) + am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithClientCertificate(testCtx, derPrivKey, cert)) + cc := allocClientCertificate() + err = rw.LookupWhere(testCtx, &cc, "ldap_method_id = ?", []any{am.GetPublicId()}) + require.NoError(t, err) + require.NoError(t, cc.decrypt(testCtx, orgDBWrapper)) + assert.NotEmpty(t, cc.GetKeyId()) + assert.NotEmpty(t, cc.GetCertificateKey()) + assert.NotEmpty(t, cc.GetCertificateKeyHmac()) + assert.NotEmpty(t, cc.GetCtCertificateKey()) + return cc.GetKeyId(), am.GetScopeId(), cc, am + }, + reader: rw, + writer: rw, + kmsRepo: testKms, + }, + { + name: "missing-key-id", + ctx: testCtx, + setup: func() (string, string, *ClientCertificate, *AuthMethod) { + _, privKey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + cert, _ := TestGenerateCA(t, "localhost") + derPrivKey, err := x509.MarshalPKCS8PrivateKey(privKey) + require.NoError(t, err) + am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithClientCertificate(testCtx, derPrivKey, cert)) + cc := allocClientCertificate() + err = rw.LookupWhere(testCtx, &cc, "ldap_method_id = ?", []any{am.GetPublicId()}) + require.NoError(t, err) + require.NoError(t, cc.decrypt(testCtx, orgDBWrapper)) + assert.NotEmpty(t, cc.GetKeyId()) + assert.NotEmpty(t, cc.GetCertificateKey()) + assert.NotEmpty(t, cc.GetCertificateKeyHmac()) + assert.NotEmpty(t, cc.GetCtCertificateKey()) + return "", am.GetScopeId(), cc, am + }, + reader: rw, + writer: rw, + kmsRepo: testKms, + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "missing data key version id", + }, + { + name: "missing-scope-id", + ctx: testCtx, + setup: func() (string, string, *ClientCertificate, *AuthMethod) { + _, privKey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + cert, _ := TestGenerateCA(t, "localhost") + derPrivKey, err := x509.MarshalPKCS8PrivateKey(privKey) + require.NoError(t, err) + am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithClientCertificate(testCtx, derPrivKey, cert)) + cc := allocClientCertificate() + err = rw.LookupWhere(testCtx, &cc, "ldap_method_id = ?", []any{am.GetPublicId()}) + require.NoError(t, err) + require.NoError(t, cc.decrypt(testCtx, orgDBWrapper)) + assert.NotEmpty(t, cc.GetKeyId()) + assert.NotEmpty(t, cc.GetCertificateKey()) + assert.NotEmpty(t, cc.GetCertificateKeyHmac()) + assert.NotEmpty(t, cc.GetCtCertificateKey()) + return cc.GetKeyId(), "", cc, am + }, + reader: rw, + writer: rw, + kmsRepo: testKms, + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "missing scope id", + }, + { + name: "missing-reader", + ctx: testCtx, + setup: func() (string, string, *ClientCertificate, *AuthMethod) { + _, privKey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + cert, _ := TestGenerateCA(t, "localhost") + derPrivKey, err := x509.MarshalPKCS8PrivateKey(privKey) + require.NoError(t, err) + am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithClientCertificate(testCtx, derPrivKey, cert)) + cc := allocClientCertificate() + err = rw.LookupWhere(testCtx, &cc, "ldap_method_id = ?", []any{am.GetPublicId()}) + require.NoError(t, err) + require.NoError(t, cc.decrypt(testCtx, orgDBWrapper)) + assert.NotEmpty(t, cc.GetKeyId()) + assert.NotEmpty(t, cc.GetCertificateKey()) + assert.NotEmpty(t, cc.GetCertificateKeyHmac()) + assert.NotEmpty(t, cc.GetCtCertificateKey()) + return cc.GetKeyId(), am.GetScopeId(), cc, am + }, + reader: nil, + writer: rw, + kmsRepo: testKms, + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "missing database reader", + }, + { + name: "missing-writer", + ctx: testCtx, + setup: func() (string, string, *ClientCertificate, *AuthMethod) { + _, privKey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + cert, _ := TestGenerateCA(t, "localhost") + derPrivKey, err := x509.MarshalPKCS8PrivateKey(privKey) + require.NoError(t, err) + am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithClientCertificate(testCtx, derPrivKey, cert)) + cc := allocClientCertificate() + err = rw.LookupWhere(testCtx, &cc, "ldap_method_id = ?", []any{am.GetPublicId()}) + require.NoError(t, err) + require.NoError(t, cc.decrypt(testCtx, orgDBWrapper)) + assert.NotEmpty(t, cc.GetKeyId()) + assert.NotEmpty(t, cc.GetCertificateKey()) + assert.NotEmpty(t, cc.GetCertificateKeyHmac()) + assert.NotEmpty(t, cc.GetCtCertificateKey()) + return cc.GetKeyId(), am.GetScopeId(), cc, am + }, + reader: rw, + writer: nil, + kmsRepo: testKms, + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "missing database writer", + }, + { + name: "missing-kms", + ctx: testCtx, + setup: func() (string, string, *ClientCertificate, *AuthMethod) { + _, privKey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + cert, _ := TestGenerateCA(t, "localhost") + derPrivKey, err := x509.MarshalPKCS8PrivateKey(privKey) + require.NoError(t, err) + am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithClientCertificate(testCtx, derPrivKey, cert)) + cc := allocClientCertificate() + err = rw.LookupWhere(testCtx, &cc, "ldap_method_id = ?", []any{am.GetPublicId()}) + require.NoError(t, err) + require.NoError(t, cc.decrypt(testCtx, orgDBWrapper)) + assert.NotEmpty(t, cc.GetKeyId()) + assert.NotEmpty(t, cc.GetCertificateKey()) + assert.NotEmpty(t, cc.GetCertificateKeyHmac()) + assert.NotEmpty(t, cc.GetCtCertificateKey()) + return cc.GetKeyId(), am.GetScopeId(), cc, am + }, + reader: rw, + writer: rw, + kmsRepo: nil, + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "missing kms repository", + }, + { + name: "GetWrapper-err", + ctx: testCtx, + setup: func() (string, string, *ClientCertificate, *AuthMethod) { + _, privKey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + cert, _ := TestGenerateCA(t, "localhost") + derPrivKey, err := x509.MarshalPKCS8PrivateKey(privKey) + require.NoError(t, err) + am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithClientCertificate(testCtx, derPrivKey, cert)) + cc := allocClientCertificate() + err = rw.LookupWhere(testCtx, &cc, "ldap_method_id = ?", []any{am.GetPublicId()}) + require.NoError(t, err) + require.NoError(t, cc.decrypt(testCtx, orgDBWrapper)) + assert.NotEmpty(t, cc.GetKeyId()) + assert.NotEmpty(t, cc.GetCertificateKey()) + assert.NotEmpty(t, cc.GetCertificateKeyHmac()) + assert.NotEmpty(t, cc.GetCtCertificateKey()) + return cc.GetKeyId(), am.GetScopeId(), cc, am + }, + reader: rw, + writer: rw, + kmsRepo: &kms.MockGetWrapperer{ + GetErr: errors.New(testCtx, errors.Internal, "test", "GetWrapper error"), + }, + wantErr: true, + wantErrCode: errors.Internal, + wantErrContains: "GetWrapper error", + }, + { + name: "encrypt-err", + ctx: testCtx, + setup: func() (string, string, *ClientCertificate, *AuthMethod) { + _, privKey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + cert, _ := TestGenerateCA(t, "localhost") + derPrivKey, err := x509.MarshalPKCS8PrivateKey(privKey) + require.NoError(t, err) + am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithClientCertificate(testCtx, derPrivKey, cert)) + cc := allocClientCertificate() + err = rw.LookupWhere(testCtx, &cc, "ldap_method_id = ?", []any{am.GetPublicId()}) + require.NoError(t, err) + require.NoError(t, cc.decrypt(testCtx, orgDBWrapper)) + assert.NotEmpty(t, cc.GetKeyId()) + assert.NotEmpty(t, cc.GetCertificateKey()) + assert.NotEmpty(t, cc.GetCertificateKeyHmac()) + assert.NotEmpty(t, cc.GetCtCertificateKey()) + return cc.GetKeyId(), am.GetScopeId(), cc, am + }, + reader: rw, + writer: rw, + kmsRepo: &kms.MockGetWrapperer{ + ReturnWrapper: &kms.MockWrapper{ + EncryptErr: errors.New(testCtx, errors.Encrypt, "test", "encrypt error"), + Wrapper: orgDBWrapper, + }, + }, + wantErr: true, + wantErrCode: errors.Encrypt, + wantErrContains: "encrypt error", + }, + { + name: "decrypt-err", + ctx: testCtx, + setup: func() (string, string, *ClientCertificate, *AuthMethod) { + _, privKey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + cert, _ := TestGenerateCA(t, "localhost") + derPrivKey, err := x509.MarshalPKCS8PrivateKey(privKey) + require.NoError(t, err) + am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithClientCertificate(testCtx, derPrivKey, cert)) + cc := allocClientCertificate() + err = rw.LookupWhere(testCtx, &cc, "ldap_method_id = ?", []any{am.GetPublicId()}) + require.NoError(t, err) + require.NoError(t, cc.decrypt(testCtx, orgDBWrapper)) + assert.NotEmpty(t, cc.GetKeyId()) + assert.NotEmpty(t, cc.GetCertificateKey()) + assert.NotEmpty(t, cc.GetCertificateKeyHmac()) + assert.NotEmpty(t, cc.GetCtCertificateKey()) + return cc.GetKeyId(), am.GetScopeId(), cc, am + }, + reader: rw, + writer: rw, + kmsRepo: &kms.MockGetWrapperer{ + ReturnWrapper: &kms.MockWrapper{ + DecryptErr: errors.New(testCtx, errors.Encrypt, "test", "decrypt error"), + Wrapper: orgDBWrapper, + }, + }, + wantErr: true, + wantErrCode: errors.Decrypt, + wantErrContains: "decrypt error", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + keyId, scopeId, cc, am := tc.setup() + + // now we can rotate and rewrap + assert.NoError(testKms.RotateKeys(testCtx, org.Scope.GetPublicId())) + + // let's do this rewrapping! + err := clientCertificateRewrapFn(tc.ctx, keyId, scopeId, tc.reader, tc.writer, tc.kmsRepo) + if tc.wantErr { + require.Error(err) + if tc.wantErrCode != errors.Unknown { + assert.True(errors.Match(errors.T(tc.wantErrCode), err)) + } + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + + // fetch the new key version + kmsWrapper, err := testKms.GetWrapper(testCtx, org.Scope.GetPublicId(), kms.KeyPurposeDatabase) + assert.NoError(err) + newKeyVersion, err := kmsWrapper.KeyId(testCtx) + assert.NoError(err) + + // get the latest bind cred + latest := allocClientCertificate() + err = rw.LookupWhere(testCtx, &latest, "ldap_method_id = ?", []any{am.GetPublicId()}) + require.NoError(err) + require.NoError(latest.decrypt(testCtx, kmsWrapper)) + + // make sure the password and its hmac are correct and that it uses + // the newest key version id + assert.NotEmpty(latest.KeyId) + assert.Equal(newKeyVersion, latest.KeyId) + assert.Equal(cc.GetCertificateKey(), latest.CertificateKey) + assert.NotEqual(cc.GetCtCertificateKey(), latest.GetCtCertificateKey()) + assert.NotEmpty(latest.GetCertificateKeyHmac()) + assert.Equal(cc.GetCertificateKeyHmac(), latest.CertificateKeyHmac) + }) + } +} diff --git a/internal/auth/ldap/service_authenticate.go b/internal/auth/ldap/service_authenticate.go new file mode 100644 index 0000000000..a1b5726e07 --- /dev/null +++ b/internal/auth/ldap/service_authenticate.go @@ -0,0 +1,99 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" +) + +type ( + // AuthenticatorFactory is used by "service functions" to create a new + // ldap.Authenticator (typically an ldap.Repository) + AuthenticatorFactory func() (Authenticator, error) + + // LookupUserFactory is used by "service functions" to create a new + // LookupUser (typically an iam repo) + LookupUserFactory func() (LookupUser, error) + + // AuthTokenCreatorFactory is used by "service functions" to create a new + // AuthTokenCreator (typically an auth token repo) + AuthTokenCreatorFactory func() (AuthTokenCreator, error) +) + +// Authenticate is an ldap domain service function for handling an LDAP +// authentication flow. On success, it returns an auth token. +// +// The service operation includes: +// - Authenticate the user against the auth method's configured ldap server. +// - Use iam.(Repository).LookupUserWithLogin(...) look up the iam.User matching the Account. +// - Use the authtoken.(Repository).CreateAuthToken(...) to create a pending auth token for the authenticated user. +func Authenticate( + ctx context.Context, + authenticatorFn AuthenticatorFactory, + lookupUserFn LookupUserFactory, + tokenCreatorFn AuthTokenCreatorFactory, + authMethodId, loginName, password string, +) (*authtoken.AuthToken, error) { + const op = "ldap.Authenticate" + switch { + case authenticatorFn == nil: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing authenticator factory") + case lookupUserFn == nil: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing lookup user factory") + case tokenCreatorFn == nil: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth token creator factory") + case authMethodId == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id") + case loginName == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing login name") + case password == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing password") + } + + r, err := authenticatorFn() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + acct, err := r.Authenticate(ctx, authMethodId, loginName, password) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + l, err := lookupUserFn() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + user, err := l.LookupUserWithLogin(ctx, acct.PublicId) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + at, err := tokenCreatorFn() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + token, err := at.CreateAuthToken(ctx, user, acct.PublicId) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + return token, nil +} + +type Authenticator interface { + Authenticate(ctx context.Context, authMethodId, loginName, password string) (*Account, error) +} + +type LookupUser interface { + LookupUserWithLogin(ctx context.Context, accountId string, opt ...iam.Option) (*iam.User, error) +} + +type AuthTokenCreator interface { + CreateAuthToken(ctx context.Context, withIamUser *iam.User, withAuthAccountId string, opt ...authtoken.Option) (*authtoken.AuthToken, error) +} diff --git a/internal/auth/ldap/service_authenticate_test.go b/internal/auth/ldap/service_authenticate_test.go new file mode 100644 index 0000000000..5719a72619 --- /dev/null +++ b/internal/auth/ldap/service_authenticate_test.go @@ -0,0 +1,334 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/boundary/internal/authtoken" + "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/go-hclog" + "github.com/jimlambrt/gldap" + "github.com/jimlambrt/gldap/testdirectory" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAuthenticate(t *testing.T) { + t.Parallel() + testCtx := context.Background() + testConn, _ := db.TestSetup(t, "postgres") + testRw := db.New(testConn) + rootWrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, testConn, rootWrapper) + + // some standard factories for unit tests + authenticatorFn := func() (Authenticator, error) { + return NewRepository(testCtx, testRw, testRw, testKms) + } + lookupUserWithFn := func() (LookupUser, error) { + return iam.NewRepository(testRw, testRw, testKms) + } + tokenCreatorFn := func() (AuthTokenCreator, error) { + return authtoken.NewRepository(testRw, testRw, testKms) + } + iamRepo := iam.TestRepo(t, testConn, rootWrapper) + org, _ := iam.TestScopes(t, iamRepo) + orgDbWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + logger := hclog.New(&hclog.LoggerOptions{ + Name: "test-logger", + Level: hclog.Error, + }) + td := testdirectory.Start(t, + testdirectory.WithDefaults(t, &testdirectory.Defaults{AllowAnonymousBind: true}), + testdirectory.WithLogger(t, logger), + ) + tdCerts, err := ParseCertificates(testCtx, td.Cert()) + require.NoError(t, err) + + groups := []*gldap.Entry{ + testdirectory.NewGroup(t, "admin", []string{"alice"}), + } + users := testdirectory.NewUsers(t, []string{"alice", "bob"}, testdirectory.WithMembersOf(t, "admin")) + td.SetUsers(users...) + td.SetGroups(groups...) + + testPrimaryAuthMethod := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId, + []string{fmt.Sprintf("ldaps://127.0.0.1:%d", td.Port())}, + WithCertificates(testCtx, tdCerts...), + WithDiscoverDn(testCtx), + WithEnableGroups(testCtx), + WithUserDn(testCtx, testdirectory.DefaultUserDN), + WithGroupDn(testCtx, testdirectory.DefaultGroupDN), + ) + iam.TestSetPrimaryAuthMethod(t, iamRepo, org, testPrimaryAuthMethod.PublicId) + + testNotPrimaryAuthMethod := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId, + []string{fmt.Sprintf("ldaps://127.0.0.1:%d", td.Port())}, + WithCertificates(testCtx, tdCerts...), + WithDiscoverDn(testCtx), + WithEnableGroups(testCtx), + WithUserDn(testCtx, testdirectory.DefaultUserDN), + WithGroupDn(testCtx, testdirectory.DefaultGroupDN), + ) + + const ( + testLoginName = "alice" + testPassword = "password" + testLoginName2 = "bob" + ) + + testExistingAcct := TestAccount(t, testConn, testNotPrimaryAuthMethod, testLoginName2) + iam.TestUser(t, iamRepo, org.PublicId, iam.WithAccountIds(testExistingAcct.PublicId)) + + // order is important in these tests, so we're using a slice + tests := []struct { + order int + name string + ctx context.Context + authenticatorFn AuthenticatorFactory + lookupUserWithLoginFn LookupUserFactory + tokenCreatorFn AuthTokenCreatorFactory + authMethodId string + loginName string + password string + wantErrMatch *errors.Template + wantErrContains string + }{ + { + order: 0, + name: "success-with-primary-auth-method-auto-create", + ctx: testCtx, + authenticatorFn: authenticatorFn, + lookupUserWithLoginFn: lookupUserWithFn, + tokenCreatorFn: tokenCreatorFn, + authMethodId: testPrimaryAuthMethod.PublicId, + loginName: testLoginName, + password: testPassword, + }, + { + order: 1, + name: "success-with-primary-auth-method-existing-account", + ctx: testCtx, + authenticatorFn: authenticatorFn, + lookupUserWithLoginFn: lookupUserWithFn, + tokenCreatorFn: tokenCreatorFn, + authMethodId: testPrimaryAuthMethod.PublicId, + loginName: testLoginName, + password: testPassword, + }, + { + order: 2, + name: "success-with-non-primary-auth-method", + ctx: testCtx, + authenticatorFn: authenticatorFn, + lookupUserWithLoginFn: lookupUserWithFn, + tokenCreatorFn: tokenCreatorFn, + authMethodId: testNotPrimaryAuthMethod.PublicId, + loginName: testLoginName2, + password: testPassword, + }, + { + order: 3, + name: "err-not-primary-refused-to-create-user", + ctx: testCtx, + authenticatorFn: authenticatorFn, + lookupUserWithLoginFn: lookupUserWithFn, + tokenCreatorFn: tokenCreatorFn, + authMethodId: testNotPrimaryAuthMethod.PublicId, + loginName: testLoginName, + password: testPassword, + wantErrMatch: errors.T(errors.RecordNotFound), + wantErrContains: "auth method is not primary for the scope so refusing to auto-create user", + }, + { + order: 4, + name: "missing-authenticator-fn", + ctx: testCtx, + lookupUserWithLoginFn: lookupUserWithFn, + tokenCreatorFn: tokenCreatorFn, + authMethodId: testNotPrimaryAuthMethod.PublicId, + loginName: testLoginName, + password: testPassword, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing authenticator factory", + }, + { + order: 5, + name: "missing-lookup-user-fn", + ctx: testCtx, + authenticatorFn: authenticatorFn, + tokenCreatorFn: tokenCreatorFn, + authMethodId: testNotPrimaryAuthMethod.PublicId, + loginName: testLoginName, + password: testPassword, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing lookup user factory", + }, + { + order: 6, + name: "missing-at-creator-fn", + ctx: testCtx, + authenticatorFn: authenticatorFn, + lookupUserWithLoginFn: lookupUserWithFn, + authMethodId: testNotPrimaryAuthMethod.PublicId, + loginName: testLoginName, + password: testPassword, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing auth token creator factory", + }, + { + order: 7, + name: "missing-auth-method-id", + ctx: testCtx, + authenticatorFn: authenticatorFn, + lookupUserWithLoginFn: lookupUserWithFn, + tokenCreatorFn: tokenCreatorFn, + loginName: testLoginName, + password: testPassword, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing auth method id", + }, + { + order: 8, + name: "missing-login-name", + ctx: testCtx, + authenticatorFn: authenticatorFn, + lookupUserWithLoginFn: lookupUserWithFn, + tokenCreatorFn: tokenCreatorFn, + authMethodId: testNotPrimaryAuthMethod.PublicId, + password: testPassword, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing login name", + }, + { + order: 9, + name: "missing-password", + ctx: testCtx, + authenticatorFn: authenticatorFn, + lookupUserWithLoginFn: lookupUserWithFn, + tokenCreatorFn: tokenCreatorFn, + authMethodId: testNotPrimaryAuthMethod.PublicId, + loginName: testLoginName, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing password", + }, + { + order: 10, + name: "authenticator-fn-err", + ctx: testCtx, + authenticatorFn: func() (Authenticator, error) { + return nil, errors.New(testCtx, errors.Internal, "test", "authenticator-fn-err") + }, + lookupUserWithLoginFn: lookupUserWithFn, + tokenCreatorFn: tokenCreatorFn, + authMethodId: testPrimaryAuthMethod.PublicId, + loginName: testLoginName, + password: testPassword, + wantErrMatch: errors.T(errors.Internal), + wantErrContains: "authenticator-fn-err", + }, + { + order: 11, + name: "lookup-user-fn-err", + ctx: testCtx, + authenticatorFn: authenticatorFn, + lookupUserWithLoginFn: func() (LookupUser, error) { + return nil, errors.New(testCtx, errors.Internal, "test", "lookup-user-fn-err") + }, + tokenCreatorFn: tokenCreatorFn, + authMethodId: testPrimaryAuthMethod.PublicId, + loginName: testLoginName, + password: testPassword, + wantErrMatch: errors.T(errors.Internal), + wantErrContains: "lookup-user-fn-err", + }, + { + order: 12, + name: "at-fn-err", + ctx: testCtx, + authenticatorFn: authenticatorFn, + lookupUserWithLoginFn: lookupUserWithFn, + tokenCreatorFn: func() (AuthTokenCreator, error) { + return nil, errors.New(testCtx, errors.Internal, "test", "at-fn-err") + }, + authMethodId: testPrimaryAuthMethod.PublicId, + loginName: testLoginName, + password: testPassword, + wantErrMatch: errors.T(errors.Internal), + wantErrContains: "at-fn-err", + }, + { + order: 13, + name: "authenticate-err", + ctx: testCtx, + authenticatorFn: func() (Authenticator, error) { + return &mockAuthenticator{authErr: errors.New(testCtx, errors.Internal, "test", "authenticate-err")}, nil + }, + lookupUserWithLoginFn: lookupUserWithFn, + tokenCreatorFn: tokenCreatorFn, + authMethodId: testPrimaryAuthMethod.PublicId, + loginName: testLoginName, + password: testPassword, + wantErrMatch: errors.T(errors.Internal), + wantErrContains: "authenticate-err", + }, + { + order: 14, + name: "token-err", + ctx: testCtx, + authenticatorFn: authenticatorFn, + lookupUserWithLoginFn: lookupUserWithFn, + tokenCreatorFn: func() (AuthTokenCreator, error) { + return &mockTokenCreator{tokenErr: errors.New(testCtx, errors.Internal, "test", "token-err")}, nil + }, + authMethodId: testPrimaryAuthMethod.PublicId, + loginName: testLoginName, + password: testPassword, + wantErrMatch: errors.T(errors.Internal), + wantErrContains: "token-err", + }, + } + for idx, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + require.Equal(tc.order, idx) + got, err := Authenticate(tc.ctx, tc.authenticatorFn, tc.lookupUserWithLoginFn, tc.tokenCreatorFn, tc.authMethodId, tc.loginName, tc.password) + if tc.wantErrMatch != nil { + require.Error(err) + assert.Empty(got) + assert.Truef(errors.Match(tc.wantErrMatch, err), "unexpected error") + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.NotEmpty(got) + }) + } +} + +type mockAuthenticator struct { + authErr error +} + +func (m *mockAuthenticator) Authenticate(ctx context.Context, authMethodId, loginName, password string) (*Account, error) { + return nil, m.authErr +} + +type mockTokenCreator struct { + tokenErr error +} + +func (m *mockTokenCreator) CreateAuthToken(ctx context.Context, withIamUser *iam.User, withAuthAccountId string, opt ...authtoken.Option) (*authtoken.AuthToken, error) { + return nil, m.tokenErr +} diff --git a/internal/auth/ldap/state.go b/internal/auth/ldap/state.go new file mode 100644 index 0000000000..ccbf8d57bc --- /dev/null +++ b/internal/auth/ldap/state.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +// AuthMethodState defines the possible states for an ldap auth method +type AuthMethodState string + +const ( + UnknownState AuthMethodState = "unknown" + InactiveState AuthMethodState = "inactive" + ActivePrivateState AuthMethodState = "active-private" + ActivePublicState AuthMethodState = "active-public" +) + +func validState(s string) bool { + st := AuthMethodState(s) + switch st { + case InactiveState, ActivePrivateState, ActivePublicState: + return true + default: + return false + } +} + +func (s AuthMethodState) String() string { + return string(s) +} diff --git a/internal/auth/ldap/state_test.go b/internal/auth/ldap/state_test.go new file mode 100644 index 0000000000..1c65028a79 --- /dev/null +++ b/internal/auth/ldap/state_test.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import "testing" + +func Test_validState(t *testing.T) { + tests := []struct { + s string + want bool + }{ + {"bad", false}, + {"", false}, + {"unknown", false}, + {"inactive", true}, + {"active-private", true}, + {"active-public", true}, + } + for _, tt := range tests { + t.Run(tt.s, func(t *testing.T) { + if got := validState(tt.s); got != tt.want { + t.Errorf("validState() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/auth/ldap/store/ldap.pb.go b/internal/auth/ldap/store/ldap.pb.go new file mode 100644 index 0000000000..d01792052a --- /dev/null +++ b/internal/auth/ldap/store/ldap.pb.go @@ -0,0 +1,2012 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc (unknown) +// source: controller/storage/auth/ldap/store/v1/ldap.proto + +// Package store provides protobufs for storing types in the ldap package. + +package store + +import ( + timestamp "github.com/hashicorp/boundary/internal/db/timestamp" + _ "github.com/hashicorp/boundary/sdk/pbs/controller/protooptions" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// AuthMethod represents an LDAP auth method. +type AuthMethod struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // public_id is the PK and is the external public identifier of the auth + // method. + // @inject_tag: `gorm:"primary_key"` + PublicId string `protobuf:"bytes,10,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" gorm:"primary_key"` + // create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,20,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + UpdateTime *timestamp.Timestamp `protobuf:"bytes,30,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"` + // name is optional. If set, it must be unique within scope_id. + // @inject_tag: `gorm:"default:null"` + Name string `protobuf:"bytes,40,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` + // description is optional. + // @inject_tag: `gorm:"default:null"` + Description string `protobuf:"bytes,50,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"` + // The scope_id of the owning scope. Must be set. + // @inject_tag: `gorm:"not_null"` + ScopeId string `protobuf:"bytes,60,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"not_null"` + // @inject_tag: `gorm:"default:null"` + Version uint32 `protobuf:"varint,70,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"` + // operational_state is the current state of the auth_ldap_method (inactive, + // active-private, or active-public). + // @inject_tag: `gorm:"column:state;not_null"` + OperationalState string `protobuf:"bytes,80,opt,name=operational_state,json=operationalState,proto3" json:"operational_state,omitempty" gorm:"column:state;not_null"` + // start_tls if true, issues a StartTLS command after establishing an + // unencrypted connection. Defaults to false. + // @inject_tag: `gorm:"not_null"` + StartTls bool `protobuf:"varint,90,opt,name=start_tls,json=startTls,proto3" json:"start_tls,omitempty" gorm:"not_null"` + // insecure_tls if true, skips LDAP server SSL certificate validation - + // insecure and use with caution. Defaults to false. + // @inject_tag: `gorm:"not_null;default:false"` + InsecureTls bool `protobuf:"varint,100,opt,name=insecure_tls,json=insecureTls,proto3" json:"insecure_tls,omitempty" gorm:"not_null;default:false"` + // discover_dn if true, use anon bind to discover the bind DN of a user. + // Defaults to false. + // @inject_tag: `gorm:"not_null;default:false"` + DiscoverDn bool `protobuf:"varint,110,opt,name=discover_dn,json=discoverDn,proto3" json:"discover_dn,omitempty" gorm:"not_null;default:false"` + // anon_group_search if true, use anon bind when performing LDAP group + // searches. Defaults to false. + // @inject_tag: `gorm:"not_null;default:false"` + AnonGroupSearch bool `protobuf:"varint,120,opt,name=anon_group_search,json=anonGroupSearch,proto3" json:"anon_group_search,omitempty" gorm:"not_null;default:false"` + // upn_domain is the userPrincipalDomain used to construct the UPN string for + // the authenticating user. The constructed UPN will appear as + // [username]@UPNDomain Example: example.com, which will cause Boundary to + // bind as username@example.com when authenticating the user. + // @inject_tag: `gorm:"default:null"` + UpnDomain string `protobuf:"bytes,130,opt,name=upn_domain,json=upnDomain,proto3" json:"upn_domain,omitempty" gorm:"default:null"` + // urls are the LDAP URLS that specify LDAP servers to connection to. There + // must be at lease on URL for each LDAP auth method. When attempting to + // connect, the URLs are tried in the order specified. These are Value Objects + // that will be stored as Url messages, and are operated on as a complete set + // (not individually). + // @inject_tag: `gorm:"-"` + Urls []string `protobuf:"bytes,140,rep,name=urls,proto3" json:"urls,omitempty" gorm:"-"` + // user_dn (optional) is the base DN under which to perform user search. + // Example: ou=Users,dc=example,dc=com + // @inject_tag: `gorm:"-"` + UserDn string `protobuf:"bytes,150,opt,name=user_dn,json=userDn,proto3" json:"user_dn,omitempty" gorm:"-"` + // user_attr (optional) is the attribute on user's entry matching the username + // passed when authenticating. Examples: cn, uid + // @inject_tag: `gorm:"-"` + UserAttr string `protobuf:"bytes,160,opt,name=user_attr,json=userAttr,proto3" json:"user_attr,omitempty" gorm:"-"` + // user_filter (optional) is a go template used to construct a LDAP user + // search filter. The template can access the following context variables: + // [UserAttr, Username]. The default userfilter is + // ({{.UserAttr}}={{.Username}}) or + // (userPrincipalName={{.Username}}@UPNDomain) if the upndomain parameter is + // set. + // @inject_tag: `gorm:"-"` + UserFilter string `protobuf:"bytes,170,opt,name=user_filter,json=userFilter,proto3" json:"user_filter,omitempty" gorm:"-"` + // enable_groups if true, an authenticated user's groups will be found during + // authentication. Defaults to false. + // @inject_tag: `gorm:"not_null;default:false"` + EnableGroups bool `protobuf:"varint,175,opt,name=enable_groups,json=enableGroups,proto3" json:"enable_groups,omitempty" gorm:"not_null;default:false"` + // group_dn (optional) is the base DN under which to perform group search. + // Example: ou=Groups,dc=example,dc=com + // + // Note: there is no default, so no base dn will be used for group searches if + // it's not specified. + // @inject_tag: `gorm:"-"` + GroupDn string `protobuf:"bytes,180,opt,name=group_dn,json=groupDn,proto3" json:"group_dn,omitempty" gorm:"-"` + // group_attr (optional) is the LDAP attribute to follow on objects returned + // by GroupFilter in order to enumerate user group membership. Examples: for + // GroupFilter queries returning group objects, use: cn. For queries returning + // user objects, use: memberOf. The default is cn. + // @inject_tag: `gorm:"-"` + GroupAttr string `protobuf:"bytes,190,opt,name=group_attr,json=groupAttr,proto3" json:"group_attr,omitempty" gorm:"-"` + // group_filter (optional) is a Go template used when constructing the group + // membership query. The template can access the following context variables: + // [UserDN, Username]. The default is + // (|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}})), + // which is compatible with several common directory schemas. + // @inject_tag: `gorm:"-"` + GroupFilter string `protobuf:"bytes,200,opt,name=group_filter,json=groupFilter,proto3" json:"group_filter,omitempty" gorm:"-"` + // certificates are optional PEM encoded x509 certificates in ASN.1 DER form + // that can be used as trust anchors when connecting to an LDAP provider. + // These are Value Objects that will be stored as Certificate messages, and + // are operated on as a complete set (not individually). + // @inject_tag: `gorm:"-"` + Certificates []string `protobuf:"bytes,210,rep,name=certificates,proto3" json:"certificates,omitempty" gorm:"-"` + // client_certificate is the certificate in ASN.1 DER form encoded as PEM. It + // must be set. + // @inject_tag: `gorm:"-"` + ClientCertificate string `protobuf:"bytes,220,opt,name=client_certificate,json=clientCertificate,proto3" json:"client_certificate,omitempty" gorm:"-"` + // client_certificate_key (optional) is the plain-text of the certificate key + // data in PKCS #8, ASN.1 DER form. We are not storing this plain-text key in + // the database. + // @inject_tag: `gorm:"-"` + ClientCertificateKey []byte `protobuf:"bytes,230,opt,name=client_certificate_key,json=clientCertificateKey,proto3" json:"client_certificate_key,omitempty" gorm:"-"` + // client_certificate_key_hmac is a sha256-hmac of the unencrypted + // client_certificate_key_hmac that is returned from the API for read. It is + // recalculated everytime the raw client_certificate_key_hmac is updated in + // the database. + // @inject_tag: `gorm:"-"` + ClientCertificateKeyHmac []byte `protobuf:"bytes,240,opt,name=client_certificate_key_hmac,json=clientCertificateKeyHmac,proto3" json:"client_certificate_key_hmac,omitempty" gorm:"-"` + // bind_dn (optional) is the distinguished name of entry to bind when + // performing user and group search. Example: + // cn=vault,ou=Users,dc=example,dc=com + // @inject_tag: `gorm:"-"` + BindDn string `protobuf:"bytes,250,opt,name=bind_dn,json=bindDn,proto3" json:"bind_dn,omitempty" gorm:"-"` + // bind_password (optional) is the password to use along with binddn when + // performing user search. (This plaintext is not stored in the database) + // @inject_tag: `gorm:"-"` + BindPassword string `protobuf:"bytes,260,opt,name=bind_password,json=bindPassword,proto3" json:"bind_password,omitempty" gorm:"-"` + // bind_password_hmac is a sha256-hmac of the unencrypted bind_password that + // is returned from the API for read. It is recalculated everytime the raw + // password is updated in the database. + // @inject_tag: `gorm:"-"` + BindPasswordHmac []byte `protobuf:"bytes,270,opt,name=bind_password_hmac,json=bindPasswordHmac,proto3" json:"bind_password_hmac,omitempty" gorm:"-"` + // is_primary_auth_method is a read-only output field which indicates if the + // auth method is set as the scope's primary auth method. + // @inject_tag: `gorm:"-"` + IsPrimaryAuthMethod bool `protobuf:"varint,280,opt,name=is_primary_auth_method,json=isPrimaryAuthMethod,proto3" json:"is_primary_auth_method,omitempty" gorm:"-"` + // use_token_groups if true, use the Active Directory tokenGroups constructed + // attribute of the user to find the group memberships. This will find all + // security groups including nested ones. + // @inject_tag: `gorm:"not_null;default:false"` + UseTokenGroups bool `protobuf:"varint,290,opt,name=use_token_groups,json=useTokenGroups,proto3" json:"use_token_groups,omitempty" gorm:"not_null;default:false"` + // account_attribute_maps are optional attribute maps from custom attributes + // to the standard attributes of fullname and email. These maps are + // represented as key=value where the key equals the from_attribute and the + // value equals the to_attribute. For example "preferredName=fullName". All + // attribute names are case insensitive. + // @inject_tag: `gorm:"-"` + AccountAttributeMaps []string `protobuf:"bytes,300,rep,name=account_attribute_maps,json=accountAttributeMaps,proto3" json:"account_attribute_maps,omitempty" gorm:"-"` +} + +func (x *AuthMethod) Reset() { + *x = AuthMethod{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AuthMethod) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthMethod) ProtoMessage() {} + +func (x *AuthMethod) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthMethod.ProtoReflect.Descriptor instead. +func (*AuthMethod) Descriptor() ([]byte, []int) { + return file_controller_storage_auth_ldap_store_v1_ldap_proto_rawDescGZIP(), []int{0} +} + +func (x *AuthMethod) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *AuthMethod) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *AuthMethod) GetUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *AuthMethod) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *AuthMethod) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *AuthMethod) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +func (x *AuthMethod) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *AuthMethod) GetOperationalState() string { + if x != nil { + return x.OperationalState + } + return "" +} + +func (x *AuthMethod) GetStartTls() bool { + if x != nil { + return x.StartTls + } + return false +} + +func (x *AuthMethod) GetInsecureTls() bool { + if x != nil { + return x.InsecureTls + } + return false +} + +func (x *AuthMethod) GetDiscoverDn() bool { + if x != nil { + return x.DiscoverDn + } + return false +} + +func (x *AuthMethod) GetAnonGroupSearch() bool { + if x != nil { + return x.AnonGroupSearch + } + return false +} + +func (x *AuthMethod) GetUpnDomain() string { + if x != nil { + return x.UpnDomain + } + return "" +} + +func (x *AuthMethod) GetUrls() []string { + if x != nil { + return x.Urls + } + return nil +} + +func (x *AuthMethod) GetUserDn() string { + if x != nil { + return x.UserDn + } + return "" +} + +func (x *AuthMethod) GetUserAttr() string { + if x != nil { + return x.UserAttr + } + return "" +} + +func (x *AuthMethod) GetUserFilter() string { + if x != nil { + return x.UserFilter + } + return "" +} + +func (x *AuthMethod) GetEnableGroups() bool { + if x != nil { + return x.EnableGroups + } + return false +} + +func (x *AuthMethod) GetGroupDn() string { + if x != nil { + return x.GroupDn + } + return "" +} + +func (x *AuthMethod) GetGroupAttr() string { + if x != nil { + return x.GroupAttr + } + return "" +} + +func (x *AuthMethod) GetGroupFilter() string { + if x != nil { + return x.GroupFilter + } + return "" +} + +func (x *AuthMethod) GetCertificates() []string { + if x != nil { + return x.Certificates + } + return nil +} + +func (x *AuthMethod) GetClientCertificate() string { + if x != nil { + return x.ClientCertificate + } + return "" +} + +func (x *AuthMethod) GetClientCertificateKey() []byte { + if x != nil { + return x.ClientCertificateKey + } + return nil +} + +func (x *AuthMethod) GetClientCertificateKeyHmac() []byte { + if x != nil { + return x.ClientCertificateKeyHmac + } + return nil +} + +func (x *AuthMethod) GetBindDn() string { + if x != nil { + return x.BindDn + } + return "" +} + +func (x *AuthMethod) GetBindPassword() string { + if x != nil { + return x.BindPassword + } + return "" +} + +func (x *AuthMethod) GetBindPasswordHmac() []byte { + if x != nil { + return x.BindPasswordHmac + } + return nil +} + +func (x *AuthMethod) GetIsPrimaryAuthMethod() bool { + if x != nil { + return x.IsPrimaryAuthMethod + } + return false +} + +func (x *AuthMethod) GetUseTokenGroups() bool { + if x != nil { + return x.UseTokenGroups + } + return false +} + +func (x *AuthMethod) GetAccountAttributeMaps() []string { + if x != nil { + return x.AccountAttributeMaps + } + return nil +} + +// Url represents LDAP URLs that specify LDAP servers to connection to. There +// must be at lease on URL for each LDAP auth method. +type Url struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,10,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // ldap_method_id is the FK to the URL's LDAP auth method. + // @inject_tag: `gorm:"primary_key"` + LdapMethodId string `protobuf:"bytes,20,opt,name=ldap_method_id,json=ldapMethodId,proto3" json:"ldap_method_id,omitempty" gorm:"primary_key"` + // connection_priority represents the priority (aka order) of the url in the + // list of ldap urls for the auth method. + // @inject_tag: `gorm:"primary_key"` + ConnectionPriority uint32 `protobuf:"varint,30,opt,name=connection_priority,json=connectionPriority,proto3" json:"connection_priority,omitempty" gorm:"primary_key"` + // server_url is the LDAP server URL. The URL scheme must be either ldap or ldaps. + // The port is optional.If no port is specified, then a default of 389 is used + // for ldap and a default of 689 is used for ldaps. (see rfc4516 for more + // information about LDAP URLs) + // @inject_tag: `gorm:"column:url;not_null"` + ServerUrl string `protobuf:"bytes,40,opt,name=server_url,json=serverUrl,proto3" json:"server_url,omitempty" gorm:"column:url;not_null"` +} + +func (x *Url) Reset() { + *x = Url{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Url) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Url) ProtoMessage() {} + +func (x *Url) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Url.ProtoReflect.Descriptor instead. +func (*Url) Descriptor() ([]byte, []int) { + return file_controller_storage_auth_ldap_store_v1_ldap_proto_rawDescGZIP(), []int{1} +} + +func (x *Url) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *Url) GetLdapMethodId() string { + if x != nil { + return x.LdapMethodId + } + return "" +} + +func (x *Url) GetConnectionPriority() uint32 { + if x != nil { + return x.ConnectionPriority + } + return 0 +} + +func (x *Url) GetServerUrl() string { + if x != nil { + return x.ServerUrl + } + return "" +} + +// UserEntrySearchConf represent a set of optional configuration fields used to +// search for user entries. +type UserEntrySearchConf struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,10,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // ldap_method_id is the FK to the UserEntrySearchConf's LDAP auth method. + // @inject_tag: `gorm:"primary_key"` + LdapMethodId string `protobuf:"bytes,20,opt,name=ldap_method_id,json=ldapMethodId,proto3" json:"ldap_method_id,omitempty" gorm:"primary_key"` + // user_dn is the base DN under which to perform user search. Example: + // ou=Users,dc=example,dc=com + // @inject_tag: `gorm:"default:null"` + UserDn string `protobuf:"bytes,30,opt,name=user_dn,json=userDn,proto3" json:"user_dn,omitempty" gorm:"default:null"` + // user_attr is the attribute on user attribute entry matching the username + // passed when authenticating. Examples: cn, uid + // @inject_tag: `gorm:"default:null"` + UserAttr string `protobuf:"bytes,40,opt,name=user_attr,json=userAttr,proto3" json:"user_attr,omitempty" gorm:"default:null"` + // user_filter is a go template used to construct a LDAP user search filter. + // The template can access the following context variables: [UserAttr, + // Username]. The default userfilter is ({{.UserAttr}}={{.Username}}) or + // (userPrincipalName={{.Username}}@UPNDomain) if the upndomain parameter is + // set. + // @inject_tag: `gorm:"default:null"` + UserFilter string `protobuf:"bytes,50,opt,name=user_filter,json=userFilter,proto3" json:"user_filter,omitempty" gorm:"default:null"` +} + +func (x *UserEntrySearchConf) Reset() { + *x = UserEntrySearchConf{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UserEntrySearchConf) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserEntrySearchConf) ProtoMessage() {} + +func (x *UserEntrySearchConf) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserEntrySearchConf.ProtoReflect.Descriptor instead. +func (*UserEntrySearchConf) Descriptor() ([]byte, []int) { + return file_controller_storage_auth_ldap_store_v1_ldap_proto_rawDescGZIP(), []int{2} +} + +func (x *UserEntrySearchConf) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *UserEntrySearchConf) GetLdapMethodId() string { + if x != nil { + return x.LdapMethodId + } + return "" +} + +func (x *UserEntrySearchConf) GetUserDn() string { + if x != nil { + return x.UserDn + } + return "" +} + +func (x *UserEntrySearchConf) GetUserAttr() string { + if x != nil { + return x.UserAttr + } + return "" +} + +func (x *UserEntrySearchConf) GetUserFilter() string { + if x != nil { + return x.UserFilter + } + return "" +} + +// GroupEntrySearchConf represent a set of optional configuration fields used to +// search for group entries. +type GroupEntrySearchConf struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,10,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // ldap_method_id is the FK to the GroupEntrySearchConf's LDAP auth method. + // @inject_tag: `gorm:"primary_key"` + LdapMethodId string `protobuf:"bytes,20,opt,name=ldap_method_id,json=ldapMethodId,proto3" json:"ldap_method_id,omitempty" gorm:"primary_key"` + // group_dn is the base DN under which to perform user search. Example: + // ou=Groups,dc=example,dc=com + // @inject_tag: `gorm:"default:null"` + GroupDn string `protobuf:"bytes,30,opt,name=group_dn,json=groupDn,proto3" json:"group_dn,omitempty" gorm:"default:null"` + // group_attr is the LDAP attribute to follow on objects returned by + // GroupFilter in order to enumerate user group membership. Examples: for + // GroupFilter queries returning group objects, use: cn. For queries returning + // user objects, use: memberOf. The default is cn. + // @inject_tag: `gorm:"default:null"` + GroupAttr string `protobuf:"bytes,40,opt,name=group_attr,json=groupAttr,proto3" json:"group_attr,omitempty" gorm:"default:null"` + // user_filter is a Go template used when constructing the group membership + // query. The template can access the following context variables: [UserDN, + // Username]. The default is + // (|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}})), + // which is compatible with several common directory schemas. + // @inject_tag: `gorm:"default:null"` + GroupFilter string `protobuf:"bytes,50,opt,name=group_filter,json=groupFilter,proto3" json:"group_filter,omitempty" gorm:"default:null"` +} + +func (x *GroupEntrySearchConf) Reset() { + *x = GroupEntrySearchConf{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GroupEntrySearchConf) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GroupEntrySearchConf) ProtoMessage() {} + +func (x *GroupEntrySearchConf) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GroupEntrySearchConf.ProtoReflect.Descriptor instead. +func (*GroupEntrySearchConf) Descriptor() ([]byte, []int) { + return file_controller_storage_auth_ldap_store_v1_ldap_proto_rawDescGZIP(), []int{3} +} + +func (x *GroupEntrySearchConf) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *GroupEntrySearchConf) GetLdapMethodId() string { + if x != nil { + return x.LdapMethodId + } + return "" +} + +func (x *GroupEntrySearchConf) GetGroupDn() string { + if x != nil { + return x.GroupDn + } + return "" +} + +func (x *GroupEntrySearchConf) GetGroupAttr() string { + if x != nil { + return x.GroupAttr + } + return "" +} + +func (x *GroupEntrySearchConf) GetGroupFilter() string { + if x != nil { + return x.GroupFilter + } + return "" +} + +// Certificate entries are optional PEM encoded x509 certificates. Each entry is +// a single certificate. An ldap auth method may have 0 or more of these +// optional x509s. If an auth method has any cert entries, they are used as +// trust anchors when connecting to the auth method's ldap provider (instead of +// the host system's cert chain). +type Certificate struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,10,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // ldap_method_id is the FK to the Certificate's LDAP auth method. + // @inject_tag: `gorm:"primary_key"` + LdapMethodId string `protobuf:"bytes,20,opt,name=ldap_method_id,json=ldapMethodId,proto3" json:"ldap_method_id,omitempty" gorm:"primary_key"` + // certificate is a PEM encoded x509 in ASN.1 DER form. + // @inject_tag: `gorm:"column:certificate;primary_key"` + Cert string `protobuf:"bytes,30,opt,name=cert,proto3" json:"cert,omitempty" gorm:"column:certificate;primary_key"` +} + +func (x *Certificate) Reset() { + *x = Certificate{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Certificate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Certificate) ProtoMessage() {} + +func (x *Certificate) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Certificate.ProtoReflect.Descriptor instead. +func (*Certificate) Descriptor() ([]byte, []int) { + return file_controller_storage_auth_ldap_store_v1_ldap_proto_rawDescGZIP(), []int{4} +} + +func (x *Certificate) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *Certificate) GetLdapMethodId() string { + if x != nil { + return x.LdapMethodId + } + return "" +} + +func (x *Certificate) GetCert() string { + if x != nil { + return x.Cert + } + return "" +} + +// ClientCertificate represent a set of optional configuration fields used for +// specifying a mTLS client cert for LDAP connections. +type ClientCertificate struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,10,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // ldap_method_id is the FK to the ClientCertificate's LDAP auth method. + // @inject_tag: `gorm:"primary_key"` + LdapMethodId string `protobuf:"bytes,20,opt,name=ldap_method_id,json=ldapMethodId,proto3" json:"ldap_method_id,omitempty" gorm:"primary_key"` + // certificate is the PEM encoded certificate in ASN.1 DER. + // It must be set. + // @inject_tag: `gorm:"not_null"` + Certificate []byte `protobuf:"bytes,30,opt,name=certificate,proto3" json:"certificate,omitempty" gorm:"not_null"` + // certificate_key is the plain-text of the certificate key data in PKCS #8, + // ASN.1 DER form. We are not storing this plain-text key in the database. + // @inject_tag: `gorm:"-" wrapping:"pt,certificate_key_data"` + CertificateKey []byte `protobuf:"bytes,40,opt,name=certificate_key,json=certificateKey,proto3" json:"certificate_key,omitempty" gorm:"-" wrapping:"pt,certificate_key_data"` + // ct_certificate_key is the ciphertext of the certificate key data. It + // is stored in the database. + // @inject_tag: `gorm:"column:certificate_key;not_null" wrapping:"ct,certificate_key_data"` + CtCertificateKey []byte `protobuf:"bytes,50,opt,name=ct_certificate_key,json=ctCertificateKey,proto3" json:"ct_certificate_key,omitempty" gorm:"column:certificate_key;not_null" wrapping:"ct,certificate_key_data"` + // certificate_key_hmac is a sha256-hmac of the unencrypted certificate_key that + // is returned from the API for read. It is recalculated everytime the raw + // certificate_key is updated. + // @inject_tag: `gorm:"not_null"` + CertificateKeyHmac []byte `protobuf:"bytes,60,opt,name=certificate_key_hmac,json=certificateKeyHmac,proto3" json:"certificate_key_hmac,omitempty" gorm:"not_null"` + // The key_id of the kms database key used for encrypting this entry. + // It must be set. + // @inject_tag: `gorm:"not_null"` + KeyId string `protobuf:"bytes,70,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty" gorm:"not_null"` +} + +func (x *ClientCertificate) Reset() { + *x = ClientCertificate{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ClientCertificate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientCertificate) ProtoMessage() {} + +func (x *ClientCertificate) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientCertificate.ProtoReflect.Descriptor instead. +func (*ClientCertificate) Descriptor() ([]byte, []int) { + return file_controller_storage_auth_ldap_store_v1_ldap_proto_rawDescGZIP(), []int{5} +} + +func (x *ClientCertificate) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *ClientCertificate) GetLdapMethodId() string { + if x != nil { + return x.LdapMethodId + } + return "" +} + +func (x *ClientCertificate) GetCertificate() []byte { + if x != nil { + return x.Certificate + } + return nil +} + +func (x *ClientCertificate) GetCertificateKey() []byte { + if x != nil { + return x.CertificateKey + } + return nil +} + +func (x *ClientCertificate) GetCtCertificateKey() []byte { + if x != nil { + return x.CtCertificateKey + } + return nil +} + +func (x *ClientCertificate) GetCertificateKeyHmac() []byte { + if x != nil { + return x.CertificateKeyHmac + } + return nil +} + +func (x *ClientCertificate) GetKeyId() string { + if x != nil { + return x.KeyId + } + return "" +} + +// BindCredentail (optional) represent parameters which allow Boundary to bind +// (aka authenticate) using the credentials provided when searching for the user +// entry used to authenticate the end user. +type BindCredential struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,10,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // ldap_method_id is the FK to the BindCredential's LDAP auth method. + // @inject_tag: `gorm:"primary_key"` + LdapMethodId string `protobuf:"bytes,20,opt,name=ldap_method_id,json=ldapMethodId,proto3" json:"ldap_method_id,omitempty" gorm:"primary_key"` + // dn is the distinguished name of the entry to bind when performing + // user and group search. Example: cn=vault,ou=Users,dc=example,dc=com + // @inject_tag: `gorm:"not_null"` + Dn string `protobuf:"bytes,30,opt,name=dn,proto3" json:"dn,omitempty" gorm:"not_null"` + // password is the plain-text password to use along with dn. We are not + // storing this plain-text key in the database. + // @inject_tag: `gorm:"-" wrapping:"pt,password_data"` + Password []byte `protobuf:"bytes,40,opt,name=password,proto3" json:"password,omitempty" gorm:"-" wrapping:"pt,password_data"` + // ct_password_key is the ciphertext of the password. It is stored in the database. + // @inject_tag: `gorm:"column:password;not_null" wrapping:"ct,password_data"` + CtPassword []byte `protobuf:"bytes,50,opt,name=ct_password,json=ctPassword,proto3" json:"ct_password,omitempty" gorm:"column:password;not_null" wrapping:"ct,password_data"` + // password_hmac is a sha256-hmac of the unencrypted password that is returned + // from the API for read. It is recalculated everytime the raw password is + // updated. + // @inject_tag: `gorm:"not_null"` + PasswordHmac []byte `protobuf:"bytes,60,opt,name=password_hmac,json=passwordHmac,proto3" json:"password_hmac,omitempty" gorm:"not_null"` + // The key_id of the kms database key used for encrypting this entry. + // It must be set. + // @inject_tag: `gorm:"not_null"` + KeyId string `protobuf:"bytes,70,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty" gorm:"not_null"` +} + +func (x *BindCredential) Reset() { + *x = BindCredential{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BindCredential) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BindCredential) ProtoMessage() {} + +func (x *BindCredential) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BindCredential.ProtoReflect.Descriptor instead. +func (*BindCredential) Descriptor() ([]byte, []int) { + return file_controller_storage_auth_ldap_store_v1_ldap_proto_rawDescGZIP(), []int{6} +} + +func (x *BindCredential) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *BindCredential) GetLdapMethodId() string { + if x != nil { + return x.LdapMethodId + } + return "" +} + +func (x *BindCredential) GetDn() string { + if x != nil { + return x.Dn + } + return "" +} + +func (x *BindCredential) GetPassword() []byte { + if x != nil { + return x.Password + } + return nil +} + +func (x *BindCredential) GetCtPassword() []byte { + if x != nil { + return x.CtPassword + } + return nil +} + +func (x *BindCredential) GetPasswordHmac() []byte { + if x != nil { + return x.PasswordHmac + } + return nil +} + +func (x *BindCredential) GetKeyId() string { + if x != nil { + return x.KeyId + } + return "" +} + +// Account respresent Accounts associated with an LDAP auth method. +type Account struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // public_id is the PK and is the external public identifier of the account + // @inject_tag: `gorm:"primary_key"` + PublicId string `protobuf:"bytes,10,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" gorm:"primary_key"` + // create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,20,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + UpdateTime *timestamp.Timestamp `protobuf:"bytes,30,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"` + // auth_method_id is the FK to the Account's LDAP auth method. + // @inject_tag: `gorm:"not_null"` + AuthMethodId string `protobuf:"bytes,40,opt,name=auth_method_id,json=authMethodId,proto3" json:"auth_method_id,omitempty" gorm:"not_null"` + // name is optional. If set, it must be unique within scope_id. + // @inject_tag: `gorm:"default:null"` + Name string `protobuf:"bytes,50,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` + // description is optional. + // @inject_tag: `gorm:"default:null"` + Description string `protobuf:"bytes,60,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"` + // The scope_id of the owning scope. Must be set. The scope_id column is not + // included here as it is used only to ensure data integrity in the database + // between iam users and auth methods. + // @inject_tag: `gorm:"not_null"` + ScopeId string `protobuf:"bytes,70,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"not_null"` + // @inject_tag: `gorm:"default:null"` + Version uint32 `protobuf:"varint,80,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"` + // login_name of the authenticated user. This is the login_name (or username) + // entered by the user when authenticating (typically the uid or cn + // attribute). Account login names must be lower case. + // @inject_tag: `gorm:"not_null"` + LoginName string `protobuf:"bytes,90,opt,name=login_name,json=loginName,proto3" json:"login_name,omitempty" gorm:"not_null"` + // full_name is a string that maps to the name attribute for the authenticated + // user. This attribute is updated every time a user successfully + // authenticates. + // @inject_tag: `gorm:"default:null"` + FullName string `protobuf:"bytes,100,opt,name=full_name,json=fullName,proto3" json:"full_name,omitempty" gorm:"default:null"` + // email is a string that maps to the email address attribute for the + // authenticated user. This attribute is updated every time a user + // successfully authenticates. + // @inject_tag: `gorm:"default:null"` + Email string `protobuf:"bytes,110,opt,name=email,proto3" json:"email,omitempty" gorm:"default:null"` + // dn is the distinguished name authenticated user's entry. Will be null until + // the user's first successful authentication. This attribute is updated + // every time a user successfully authenticates. + // @inject_tag: `gorm:"default:null"` + Dn string `protobuf:"bytes,120,opt,name=dn,proto3" json:"dn,omitempty" gorm:"default:null"` + // member_of_groups are the json marshalled groups the authenticated user is a + // member of. Will be null until the user's first successful authentication. + // This attribute is updated every time a user successfully authenticates. + // @inject_tag: `gorm:"default:null"` + MemberOfGroups string `protobuf:"bytes,140,opt,name=member_of_groups,json=memberOfGroups,proto3" json:"member_of_groups,omitempty" gorm:"default:null"` +} + +func (x *Account) Reset() { + *x = Account{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Account) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Account) ProtoMessage() {} + +func (x *Account) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Account.ProtoReflect.Descriptor instead. +func (*Account) Descriptor() ([]byte, []int) { + return file_controller_storage_auth_ldap_store_v1_ldap_proto_rawDescGZIP(), []int{7} +} + +func (x *Account) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *Account) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *Account) GetUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *Account) GetAuthMethodId() string { + if x != nil { + return x.AuthMethodId + } + return "" +} + +func (x *Account) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Account) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Account) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +func (x *Account) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *Account) GetLoginName() string { + if x != nil { + return x.LoginName + } + return "" +} + +func (x *Account) GetFullName() string { + if x != nil { + return x.FullName + } + return "" +} + +func (x *Account) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *Account) GetDn() string { + if x != nil { + return x.Dn + } + return "" +} + +func (x *Account) GetMemberOfGroups() string { + if x != nil { + return x.MemberOfGroups + } + return "" +} + +// AccountAttributeMap entries are optional from/to account attribute maps. +type AccountAttributeMap struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // @inject_tag: `gorm:"primary_key"` + LdapMethodId string `protobuf:"bytes,10,opt,name=ldap_method_id,json=ldapMethodId,proto3" json:"ldap_method_id,omitempty" gorm:"primary_key"` + // from_attribute is the attribute from the user's entry that you need to map + // to a standard account attribute. + // @inject_tag: `gorm:"not_null"` + FromAttribute string `protobuf:"bytes,20,opt,name=from_attribute,json=fromAttribute,proto3" json:"from_attribute,omitempty" gorm:"not_null"` + // to_attribute is the standard account attribute to map the from_attribute + // to. Valid values are: fullname, email + // @inject_tag: `gorm:"column:to_attribute;primary_key"` + ToAttribute string `protobuf:"bytes,30,opt,name=to_attribute,json=toAttribute,proto3" json:"to_attribute,omitempty" gorm:"column:to_attribute;primary_key"` + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,40,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` +} + +func (x *AccountAttributeMap) Reset() { + *x = AccountAttributeMap{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AccountAttributeMap) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AccountAttributeMap) ProtoMessage() {} + +func (x *AccountAttributeMap) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AccountAttributeMap.ProtoReflect.Descriptor instead. +func (*AccountAttributeMap) Descriptor() ([]byte, []int) { + return file_controller_storage_auth_ldap_store_v1_ldap_proto_rawDescGZIP(), []int{8} +} + +func (x *AccountAttributeMap) GetLdapMethodId() string { + if x != nil { + return x.LdapMethodId + } + return "" +} + +func (x *AccountAttributeMap) GetFromAttribute() string { + if x != nil { + return x.FromAttribute + } + return "" +} + +func (x *AccountAttributeMap) GetToAttribute() string { + if x != nil { + return x.ToAttribute + } + return "" +} + +func (x *AccountAttributeMap) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +// ManagedGroup entries provide an LDAP auth method implementation of managed +// groups. +type ManagedGroup struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // @inject_tag: `gorm:"primary_key"` + PublicId string `protobuf:"bytes,10,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" gorm:"primary_key"` + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,20,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // The update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + UpdateTime *timestamp.Timestamp `protobuf:"bytes,30,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"` + // name is optional. If set, it must be unique within auth_method_id. + // @inject_tag: `gorm:"default:null"` + Name string `protobuf:"bytes,40,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` + // description is optional. + // @inject_tag: `gorm:"default:null"` + Description string `protobuf:"bytes,50,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"` + // @inject_tag: `gorm:"default:null"` + Version uint32 `protobuf:"varint,60,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"` + // auth_method_id is the fk to the account's auth method. + // @inject_tag: `gorm:"not_null"` + AuthMethodId string `protobuf:"bytes,70,opt,name=auth_method_id,json=authMethodId,proto3" json:"auth_method_id,omitempty" gorm:"not_null"` + // groups is json marshalled list of groups that make up the ManagedGroup + // @inject_tag: `gorm:"not_null"` + GroupNames string `protobuf:"bytes,80,opt,name=group_names,json=groupNames,proto3" json:"group_names,omitempty" gorm:"not_null"` +} + +func (x *ManagedGroup) Reset() { + *x = ManagedGroup{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ManagedGroup) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ManagedGroup) ProtoMessage() {} + +func (x *ManagedGroup) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ManagedGroup.ProtoReflect.Descriptor instead. +func (*ManagedGroup) Descriptor() ([]byte, []int) { + return file_controller_storage_auth_ldap_store_v1_ldap_proto_rawDescGZIP(), []int{9} +} + +func (x *ManagedGroup) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *ManagedGroup) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *ManagedGroup) GetUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *ManagedGroup) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ManagedGroup) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *ManagedGroup) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *ManagedGroup) GetAuthMethodId() string { + if x != nil { + return x.AuthMethodId + } + return "" +} + +func (x *ManagedGroup) GetGroupNames() string { + if x != nil { + return x.GroupNames + } + return "" +} + +// ManagedGroupMemberAccount contains a mapping between a managed group and a +// member account. +type ManagedGroupMemberAccount struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,10,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // managed_group_id is the fk to the oidc managed group public id + // @inject_tag: `gorm:"primary_key"` + ManagedGroupId string `protobuf:"bytes,20,opt,name=managed_group_id,json=managedGroupId,proto3" json:"managed_group_id,omitempty" gorm:"primary_key"` + // member_id is the fk to the oidc account public id + // @inject_tag: `gorm:"primary_key"` + MemberId string `protobuf:"bytes,30,opt,name=member_id,json=memberId,proto3" json:"member_id,omitempty" gorm:"primary_key"` +} + +func (x *ManagedGroupMemberAccount) Reset() { + *x = ManagedGroupMemberAccount{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ManagedGroupMemberAccount) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ManagedGroupMemberAccount) ProtoMessage() {} + +func (x *ManagedGroupMemberAccount) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[10] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ManagedGroupMemberAccount.ProtoReflect.Descriptor instead. +func (*ManagedGroupMemberAccount) Descriptor() ([]byte, []int) { + return file_controller_storage_auth_ldap_store_v1_ldap_proto_rawDescGZIP(), []int{10} +} + +func (x *ManagedGroupMemberAccount) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *ManagedGroupMemberAccount) GetManagedGroupId() string { + if x != nil { + return x.ManagedGroupId + } + return "" +} + +func (x *ManagedGroupMemberAccount) GetMemberId() string { + if x != nil { + return x.MemberId + } + return "" +} + +var File_controller_storage_auth_ldap_store_v1_ldap_proto protoreflect.FileDescriptor + +var file_controller_storage_auth_ldap_store_v1_ldap_proto_rawDesc = []byte{ + 0x0a, 0x30, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x6c, 0x64, 0x61, 0x70, 0x2f, 0x73, + 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x6c, 0x64, 0x61, 0x70, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x12, 0x25, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, + 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x6c, 0x64, 0x61, 0x70, + 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x2a, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x6f, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xa0, 0x11, 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x4d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, + 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, + 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, + 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x1e, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x42, 0x10, 0xc2, 0xdd, 0x29, 0x0c, + 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x32, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1e, 0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x0b, 0x44, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x64, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, + 0x18, 0x3c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x12, + 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x46, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x55, 0x0a, 0x11, 0x6f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x50, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x28, 0xc2, 0xdd, 0x29, 0x24, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x52, 0x10, + 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x12, 0x41, 0x0a, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x6c, 0x73, 0x18, 0x5a, 0x20, + 0x01, 0x28, 0x08, 0x42, 0x24, 0xc2, 0xdd, 0x29, 0x20, 0x0a, 0x08, 0x53, 0x74, 0x61, 0x72, 0x74, + 0x54, 0x6c, 0x73, 0x12, 0x14, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, + 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x6c, 0x73, 0x52, 0x08, 0x73, 0x74, 0x61, 0x72, 0x74, + 0x54, 0x6c, 0x73, 0x12, 0x4d, 0x0a, 0x0c, 0x69, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x5f, + 0x74, 0x6c, 0x73, 0x18, 0x64, 0x20, 0x01, 0x28, 0x08, 0x42, 0x2a, 0xc2, 0xdd, 0x29, 0x26, 0x0a, + 0x0b, 0x49, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x54, 0x6c, 0x73, 0x12, 0x17, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x69, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, + 0x65, 0x5f, 0x74, 0x6c, 0x73, 0x52, 0x0b, 0x69, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x54, + 0x6c, 0x73, 0x12, 0x49, 0x0a, 0x0b, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x5f, 0x64, + 0x6e, 0x18, 0x6e, 0x20, 0x01, 0x28, 0x08, 0x42, 0x28, 0xc2, 0xdd, 0x29, 0x24, 0x0a, 0x0a, 0x44, + 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x44, 0x6e, 0x12, 0x16, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x5f, 0x64, + 0x6e, 0x52, 0x0a, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x44, 0x6e, 0x12, 0x5f, 0x0a, + 0x11, 0x61, 0x6e, 0x6f, 0x6e, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x73, 0x65, 0x61, 0x72, + 0x63, 0x68, 0x18, 0x78, 0x20, 0x01, 0x28, 0x08, 0x42, 0x33, 0xc2, 0xdd, 0x29, 0x2f, 0x0a, 0x0f, + 0x41, 0x6e, 0x6f, 0x6e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, + 0x1c, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x61, 0x6e, 0x6f, 0x6e, + 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x0f, 0x61, + 0x6e, 0x6f, 0x6e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x46, + 0x0a, 0x0a, 0x75, 0x70, 0x6e, 0x5f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x82, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x26, 0xc2, 0xdd, 0x29, 0x22, 0x0a, 0x09, 0x55, 0x70, 0x6e, 0x44, 0x6f, + 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x15, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, + 0x2e, 0x75, 0x70, 0x6e, 0x5f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x09, 0x75, 0x70, 0x6e, + 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x30, 0x0a, 0x04, 0x75, 0x72, 0x6c, 0x73, 0x18, 0x8c, + 0x01, 0x20, 0x03, 0x28, 0x09, 0x42, 0x1b, 0xc2, 0xdd, 0x29, 0x17, 0x0a, 0x04, 0x55, 0x72, 0x6c, + 0x73, 0x12, 0x0f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x75, 0x72, + 0x6c, 0x73, 0x52, 0x04, 0x75, 0x72, 0x6c, 0x73, 0x12, 0x3a, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, + 0x5f, 0x64, 0x6e, 0x18, 0x96, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x20, 0xc2, 0xdd, 0x29, 0x1c, + 0x0a, 0x06, 0x55, 0x73, 0x65, 0x72, 0x44, 0x6e, 0x12, 0x12, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x73, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x64, 0x6e, 0x52, 0x06, 0x75, 0x73, + 0x65, 0x72, 0x44, 0x6e, 0x12, 0x42, 0x0a, 0x09, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x61, 0x74, 0x74, + 0x72, 0x18, 0xa0, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x24, 0xc2, 0xdd, 0x29, 0x20, 0x0a, 0x08, + 0x55, 0x73, 0x65, 0x72, 0x41, 0x74, 0x74, 0x72, 0x12, 0x14, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x73, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x52, 0x08, + 0x75, 0x73, 0x65, 0x72, 0x41, 0x74, 0x74, 0x72, 0x12, 0x4a, 0x0a, 0x0b, 0x75, 0x73, 0x65, 0x72, + 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0xaa, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x28, + 0xc2, 0xdd, 0x29, 0x24, 0x0a, 0x0a, 0x55, 0x73, 0x65, 0x72, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x12, 0x16, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x75, 0x73, 0x65, + 0x72, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x46, 0x69, + 0x6c, 0x74, 0x65, 0x72, 0x12, 0x52, 0x0a, 0x0d, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x67, + 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0xaf, 0x01, 0x20, 0x01, 0x28, 0x08, 0x42, 0x2c, 0xc2, 0xdd, + 0x29, 0x28, 0x0a, 0x0c, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, + 0x12, 0x18, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x65, 0x6e, 0x61, + 0x62, 0x6c, 0x65, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x52, 0x0c, 0x65, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x3e, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, + 0x70, 0x5f, 0x64, 0x6e, 0x18, 0xb4, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x22, 0xc2, 0xdd, 0x29, + 0x1e, 0x0a, 0x07, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x44, 0x6e, 0x12, 0x13, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x64, 0x6e, 0x52, + 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x44, 0x6e, 0x12, 0x46, 0x0a, 0x0a, 0x67, 0x72, 0x6f, 0x75, + 0x70, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x18, 0xbe, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x26, 0xc2, + 0xdd, 0x29, 0x22, 0x0a, 0x09, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x41, 0x74, 0x74, 0x72, 0x12, 0x15, + 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x67, 0x72, 0x6f, 0x75, 0x70, + 0x5f, 0x61, 0x74, 0x74, 0x72, 0x52, 0x09, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x41, 0x74, 0x74, 0x72, + 0x12, 0x4e, 0x0a, 0x0c, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x18, 0xc8, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x2a, 0xc2, 0xdd, 0x29, 0x26, 0x0a, 0x0b, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x17, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x66, 0x69, 0x6c, + 0x74, 0x65, 0x72, 0x52, 0x0b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x12, 0x50, 0x0a, 0x0c, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, + 0x18, 0xd2, 0x01, 0x20, 0x03, 0x28, 0x09, 0x42, 0x2b, 0xc2, 0xdd, 0x29, 0x27, 0x0a, 0x0c, 0x43, + 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x12, 0x17, 0x61, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x73, 0x52, 0x0c, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x73, 0x12, 0x66, 0x0a, 0x12, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x65, 0x72, + 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x18, 0xdc, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x36, 0xc2, 0xdd, 0x29, 0x32, 0x0a, 0x11, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x65, 0x72, + 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x1d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x73, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x65, 0x72, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x11, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, + 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x74, 0x0a, 0x16, 0x63, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, + 0x5f, 0x6b, 0x65, 0x79, 0x18, 0xe6, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x3d, 0xc2, 0xdd, 0x29, + 0x39, 0x0a, 0x14, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x73, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x52, 0x14, 0x63, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, + 0x12, 0x3e, 0x0a, 0x1b, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x18, + 0xf0, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x18, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x65, + 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x48, 0x6d, 0x61, 0x63, + 0x12, 0x3a, 0x0a, 0x07, 0x62, 0x69, 0x6e, 0x64, 0x5f, 0x64, 0x6e, 0x18, 0xfa, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x20, 0xc2, 0xdd, 0x29, 0x1c, 0x0a, 0x06, 0x42, 0x69, 0x6e, 0x64, 0x44, 0x6e, + 0x12, 0x12, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x62, 0x69, 0x6e, + 0x64, 0x5f, 0x64, 0x6e, 0x52, 0x06, 0x62, 0x69, 0x6e, 0x64, 0x44, 0x6e, 0x12, 0x52, 0x0a, 0x0d, + 0x62, 0x69, 0x6e, 0x64, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x84, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x2c, 0xc2, 0xdd, 0x29, 0x28, 0x0a, 0x0c, 0x42, 0x69, 0x6e, 0x64, + 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x18, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x73, 0x2e, 0x62, 0x69, 0x6e, 0x64, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, + 0x72, 0x64, 0x52, 0x0c, 0x62, 0x69, 0x6e, 0x64, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, + 0x12, 0x2d, 0x0a, 0x12, 0x62, 0x69, 0x6e, 0x64, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x18, 0x8e, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x10, 0x62, + 0x69, 0x6e, 0x64, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x48, 0x6d, 0x61, 0x63, 0x12, + 0x34, 0x0a, 0x16, 0x69, 0x73, 0x5f, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x5f, 0x61, 0x75, + 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x98, 0x02, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x13, 0x69, 0x73, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x41, 0x75, 0x74, 0x68, 0x4d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x5c, 0x0a, 0x10, 0x75, 0x73, 0x65, 0x5f, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0xa2, 0x02, 0x20, 0x01, 0x28, 0x08, + 0x42, 0x31, 0xc2, 0xdd, 0x29, 0x2d, 0x0a, 0x0e, 0x55, 0x73, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x1b, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x73, 0x2e, 0x75, 0x73, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x67, 0x72, 0x6f, + 0x75, 0x70, 0x73, 0x52, 0x0e, 0x75, 0x73, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x47, 0x72, 0x6f, + 0x75, 0x70, 0x73, 0x12, 0x74, 0x0a, 0x16, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x61, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x70, 0x73, 0x18, 0xac, 0x02, + 0x20, 0x03, 0x28, 0x09, 0x42, 0x3d, 0xc2, 0xdd, 0x29, 0x39, 0x0a, 0x14, 0x41, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4d, 0x61, 0x70, 0x73, + 0x12, 0x21, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x61, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6d, + 0x61, 0x70, 0x73, 0x52, 0x14, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x41, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x4d, 0x61, 0x70, 0x73, 0x22, 0xc8, 0x01, 0x0a, 0x03, 0x55, 0x72, + 0x6c, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x24, + 0x0a, 0x0e, 0x6c, 0x64, 0x61, 0x70, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, + 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6c, 0x64, 0x61, 0x70, 0x4d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x13, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x18, 0x1e, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x12, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x69, + 0x6f, 0x72, 0x69, 0x74, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, + 0x75, 0x72, 0x6c, 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x55, 0x72, 0x6c, 0x22, 0xdf, 0x01, 0x0a, 0x13, 0x55, 0x73, 0x65, 0x72, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x12, 0x4b, 0x0a, 0x0b, + 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, + 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x6c, 0x64, 0x61, + 0x70, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0c, 0x6c, 0x64, 0x61, 0x70, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, + 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x64, 0x6e, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x44, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x75, 0x73, 0x65, 0x72, + 0x5f, 0x61, 0x74, 0x74, 0x72, 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, + 0x72, 0x41, 0x74, 0x74, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x66, 0x69, + 0x6c, 0x74, 0x65, 0x72, 0x18, 0x32, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x75, 0x73, 0x65, 0x72, + 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0xe6, 0x01, 0x0a, 0x14, 0x47, 0x72, 0x6f, 0x75, 0x70, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x12, + 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0e, + 0x6c, 0x64, 0x61, 0x70, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x14, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6c, 0x64, 0x61, 0x70, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x64, 0x6e, 0x18, 0x1e, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x44, 0x6e, 0x12, 0x1d, 0x0a, + 0x0a, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x18, 0x28, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x41, 0x74, 0x74, 0x72, 0x12, 0x21, 0x0a, 0x0c, + 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x32, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, + 0x94, 0x01, 0x0a, 0x0b, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, + 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0e, + 0x6c, 0x64, 0x61, 0x70, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x14, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6c, 0x64, 0x61, 0x70, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x65, 0x72, 0x74, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x63, 0x65, 0x72, 0x74, 0x22, 0xc8, 0x02, 0x0a, 0x11, 0x43, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x4b, 0x0a, 0x0b, + 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, + 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x6c, 0x64, 0x61, + 0x70, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0c, 0x6c, 0x64, 0x61, 0x70, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, + 0x20, 0x0a, 0x0b, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x18, 0x1e, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, + 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x28, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x63, 0x65, 0x72, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x2c, 0x0a, 0x12, 0x63, 0x74, + 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, + 0x18, 0x32, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x10, 0x63, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x14, 0x63, 0x65, 0x72, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x68, 0x6d, 0x61, 0x63, + 0x18, 0x3c, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x12, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x48, 0x6d, 0x61, 0x63, 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x65, + 0x79, 0x5f, 0x69, 0x64, 0x18, 0x46, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, + 0x64, 0x22, 0x8c, 0x02, 0x0a, 0x0e, 0x42, 0x69, 0x6e, 0x64, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x61, 0x6c, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, + 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, + 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, + 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x6c, 0x64, 0x61, 0x70, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6c, 0x64, 0x61, 0x70, 0x4d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x64, 0x6e, 0x18, 0x1e, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x64, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, + 0x6f, 0x72, 0x64, 0x18, 0x28, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, + 0x6f, 0x72, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x74, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, + 0x72, 0x64, 0x18, 0x32, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x63, 0x74, 0x50, 0x61, 0x73, 0x73, + 0x77, 0x6f, 0x72, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, + 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x18, 0x3c, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x70, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x48, 0x6d, 0x61, 0x63, 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x65, 0x79, + 0x5f, 0x69, 0x64, 0x18, 0x46, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, + 0x22, 0x90, 0x04, 0x0a, 0x07, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1b, 0x0a, 0x09, + 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, + 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, + 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, + 0x69, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x75, 0x74, + 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x32, 0x20, 0x01, 0x28, 0x09, 0x42, 0x10, 0xc2, 0xdd, 0x29, 0x0c, 0x0a, 0x04, 0x4e, + 0x61, 0x6d, 0x65, 0x12, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, + 0x40, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x3c, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x1e, 0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x0b, 0x44, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x46, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x50, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x5a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6c, 0x6f, 0x67, 0x69, + 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x75, 0x6c, 0x6c, 0x4e, 0x61, + 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x6e, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x0e, 0x0a, 0x02, 0x64, 0x6e, 0x18, 0x78, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x64, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x6d, 0x65, 0x6d, 0x62, + 0x65, 0x72, 0x5f, 0x6f, 0x66, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x8c, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66, 0x47, 0x72, 0x6f, + 0x75, 0x70, 0x73, 0x22, 0xd2, 0x01, 0x0a, 0x13, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x41, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4d, 0x61, 0x70, 0x12, 0x24, 0x0a, 0x0e, 0x6c, + 0x64, 0x61, 0x70, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6c, 0x64, 0x61, 0x70, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, + 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x66, 0x72, 0x6f, 0x6d, 0x41, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x6f, 0x5f, 0x61, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x74, 0x6f, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x28, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, + 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, + 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x22, 0xb8, 0x03, 0x0a, 0x0c, 0x4d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, + 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, + 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, + 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, + 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, + 0x6d, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, + 0x12, 0x24, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x42, 0x10, + 0xc2, 0xdd, 0x29, 0x0c, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x32, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1e, 0xc2, 0xdd, 0x29, + 0x1a, 0x0a, 0x0b, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, + 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x3c, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x5f, 0x69, 0x64, 0x18, 0x46, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x75, 0x74, 0x68, + 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x49, 0x0a, 0x0b, 0x67, 0x72, 0x6f, 0x75, + 0x70, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x50, 0x20, 0x01, 0x28, 0x09, 0x42, 0x28, 0xc2, + 0xdd, 0x29, 0x24, 0x0a, 0x0a, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, + 0x16, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x67, 0x72, 0x6f, 0x75, + 0x70, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x52, 0x0a, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x4e, 0x61, + 0x6d, 0x65, 0x73, 0x22, 0xaf, 0x01, 0x0a, 0x19, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x28, + 0x0a, 0x10, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, + 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x65, 0x6d, 0x62, + 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x65, 0x6d, + 0x62, 0x65, 0x72, 0x49, 0x64, 0x42, 0x3e, 0x5a, 0x3c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, + 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, + 0x61, 0x75, 0x74, 0x68, 0x2f, 0x6c, 0x64, 0x61, 0x70, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, + 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_controller_storage_auth_ldap_store_v1_ldap_proto_rawDescOnce sync.Once + file_controller_storage_auth_ldap_store_v1_ldap_proto_rawDescData = file_controller_storage_auth_ldap_store_v1_ldap_proto_rawDesc +) + +func file_controller_storage_auth_ldap_store_v1_ldap_proto_rawDescGZIP() []byte { + file_controller_storage_auth_ldap_store_v1_ldap_proto_rawDescOnce.Do(func() { + file_controller_storage_auth_ldap_store_v1_ldap_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_auth_ldap_store_v1_ldap_proto_rawDescData) + }) + return file_controller_storage_auth_ldap_store_v1_ldap_proto_rawDescData +} + +var file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_controller_storage_auth_ldap_store_v1_ldap_proto_goTypes = []interface{}{ + (*AuthMethod)(nil), // 0: controller.storage.auth.ldap.store.v1.AuthMethod + (*Url)(nil), // 1: controller.storage.auth.ldap.store.v1.Url + (*UserEntrySearchConf)(nil), // 2: controller.storage.auth.ldap.store.v1.UserEntrySearchConf + (*GroupEntrySearchConf)(nil), // 3: controller.storage.auth.ldap.store.v1.GroupEntrySearchConf + (*Certificate)(nil), // 4: controller.storage.auth.ldap.store.v1.Certificate + (*ClientCertificate)(nil), // 5: controller.storage.auth.ldap.store.v1.ClientCertificate + (*BindCredential)(nil), // 6: controller.storage.auth.ldap.store.v1.BindCredential + (*Account)(nil), // 7: controller.storage.auth.ldap.store.v1.Account + (*AccountAttributeMap)(nil), // 8: controller.storage.auth.ldap.store.v1.AccountAttributeMap + (*ManagedGroup)(nil), // 9: controller.storage.auth.ldap.store.v1.ManagedGroup + (*ManagedGroupMemberAccount)(nil), // 10: controller.storage.auth.ldap.store.v1.ManagedGroupMemberAccount + (*timestamp.Timestamp)(nil), // 11: controller.storage.timestamp.v1.Timestamp +} +var file_controller_storage_auth_ldap_store_v1_ldap_proto_depIdxs = []int32{ + 11, // 0: controller.storage.auth.ldap.store.v1.AuthMethod.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 1: controller.storage.auth.ldap.store.v1.AuthMethod.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 2: controller.storage.auth.ldap.store.v1.Url.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 3: controller.storage.auth.ldap.store.v1.UserEntrySearchConf.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 4: controller.storage.auth.ldap.store.v1.GroupEntrySearchConf.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 5: controller.storage.auth.ldap.store.v1.Certificate.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 6: controller.storage.auth.ldap.store.v1.ClientCertificate.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 7: controller.storage.auth.ldap.store.v1.BindCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 8: controller.storage.auth.ldap.store.v1.Account.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 9: controller.storage.auth.ldap.store.v1.Account.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 10: controller.storage.auth.ldap.store.v1.AccountAttributeMap.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 11: controller.storage.auth.ldap.store.v1.ManagedGroup.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 12: controller.storage.auth.ldap.store.v1.ManagedGroup.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 11, // 13: controller.storage.auth.ldap.store.v1.ManagedGroupMemberAccount.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 14, // [14:14] is the sub-list for method output_type + 14, // [14:14] is the sub-list for method input_type + 14, // [14:14] is the sub-list for extension type_name + 14, // [14:14] is the sub-list for extension extendee + 0, // [0:14] is the sub-list for field type_name +} + +func init() { file_controller_storage_auth_ldap_store_v1_ldap_proto_init() } +func file_controller_storage_auth_ldap_store_v1_ldap_proto_init() { + if File_controller_storage_auth_ldap_store_v1_ldap_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AuthMethod); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Url); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UserEntrySearchConf); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GroupEntrySearchConf); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Certificate); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ClientCertificate); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BindCredential); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Account); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AccountAttributeMap); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ManagedGroup); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ManagedGroupMemberAccount); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controller_storage_auth_ldap_store_v1_ldap_proto_rawDesc, + NumEnums: 0, + NumMessages: 11, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_auth_ldap_store_v1_ldap_proto_goTypes, + DependencyIndexes: file_controller_storage_auth_ldap_store_v1_ldap_proto_depIdxs, + MessageInfos: file_controller_storage_auth_ldap_store_v1_ldap_proto_msgTypes, + }.Build() + File_controller_storage_auth_ldap_store_v1_ldap_proto = out.File + file_controller_storage_auth_ldap_store_v1_ldap_proto_rawDesc = nil + file_controller_storage_auth_ldap_store_v1_ldap_proto_goTypes = nil + file_controller_storage_auth_ldap_store_v1_ldap_proto_depIdxs = nil +} diff --git a/internal/auth/ldap/testing.go b/internal/auth/ldap/testing.go new file mode 100644 index 0000000000..a062131273 --- /dev/null +++ b/internal/auth/ldap/testing.go @@ -0,0 +1,301 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/json" + "encoding/pem" + "math/big" + "net" + "net/url" + "sort" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/db" + wrapping "github.com/hashicorp/go-kms-wrapping/v2" + "github.com/stretchr/testify/require" +) + +const TestInvalidPem = `-----BEGIN CERTIFICATE----- +MIICUTCCAfugAwIBAgIBADANBgkqhkiG9w0BAQQFADBXMQswCQYDVQQGEwJDTjEL +-----END CERTIFICATE-----` + +var testGrpNames = []string{"test-admin", "test-users"} + +// TestEncodeGrpNames will json marshal group names +func TestEncodedGrpNames(t *testing.T, names ...string) string { + encoded, err := json.Marshal(names) + require.NoError(t, err) + return string(encoded) +} + +// TestAuthMethod creates a new auth method and it's persisted in the database. +// See NewAuthMethod for list of supported options. +func TestAuthMethod(t testing.TB, + conn *db.DB, + databaseWrapper wrapping.Wrapper, + scopeId string, + urls []string, + opt ...Option, +) *AuthMethod { + t.Helper() + require.Greater(t, len(urls), 0) + testCtx := context.TODO() + require := require.New(t) + rw := db.New(conn) + + opt = append(opt, WithUrls(testCtx, TestConvertToUrls(t, urls...)...)) + + opts, err := getOpts(opt...) + require.NoError(err) + am, err := NewAuthMethod(testCtx, scopeId, opt...) + require.NoError(err) + id, err := newAuthMethodId(testCtx) + require.NoError(err) + am.PublicId = id + _, err = rw.DoTx(testCtx, 0, db.ConstBackoff{}, func(r db.Reader, w db.Writer) error { + if err := w.Create(testCtx, am); err != nil { + return err + } + for priority, u := range urls { + storeUrl, err := NewUrl(testCtx, am.PublicId, priority+1, TestConvertToUrls(t, u)[0]) + if err != nil { + return err + } + if err := w.Create(testCtx, storeUrl); err != nil { + return err + } + } + if len(opts.withCertificates) > 0 { + for _, pem := range opts.withCertificates { + c, err := NewCertificate(testCtx, am.PublicId, pem) + if err != nil { + return err + } + if err := w.Create(testCtx, c); err != nil { + return err + } + } + } + if len(opts.withAccountAttributeMap) > 0 { + for from, to := range opts.withAccountAttributeMap { + aam, err := NewAccountAttributeMap(testCtx, am.PublicId, from, AccountToAttribute(string(to))) + if err != nil { + return err + } + if err := w.Create(testCtx, aam); err != nil { + return err + } + } + } + if opts.withUserDn != "" || opts.withUserAttr != "" || opts.withUserFilter != "" { + uc, err := NewUserEntrySearchConf(testCtx, am.PublicId, opt...) + if err != nil { + return err + } + if err = w.Create(testCtx, uc); err != nil { + return err + } + } + if opts.withGroupDn != "" || opts.withGroupAttr != "" || opts.withGroupFilter != "" { + uc, err := NewGroupEntrySearchConf(testCtx, am.PublicId, opt...) + if err != nil { + return err + } + if err = w.Create(testCtx, uc); err != nil { + return err + } + } + if opts.withClientCertificate != "" || len(opts.withClientCertificateKey) != 0 { + cc, err := NewClientCertificate(testCtx, am.PublicId, opts.withClientCertificateKey, opts.withClientCertificate) + if err != nil { + return err + } + if err := cc.encrypt(testCtx, databaseWrapper); err != nil { + return err + } + if err := w.Create(testCtx, cc); err != nil { + return err + } + am.ClientCertificateKeyHmac = cc.CertificateKeyHmac + } + if opts.withBindDn != "" || opts.withBindPassword != "" { + bc, err := NewBindCredential(testCtx, am.PublicId, opts.withBindDn, []byte(opts.withBindPassword)) + if err != nil { + return err + } + if err := bc.encrypt(testCtx, databaseWrapper); err != nil { + return err + } + if err := w.Create(testCtx, bc); err != nil { + return err + } + am.BindPasswordHmac = bc.PasswordHmac + } + return nil + }) + require.NoError(err) + + return am +} + +// TestAccount creates a test ldap auth account. +func TestAccount(t testing.TB, conn *db.DB, am *AuthMethod, loginName string, opt ...Option) *Account { + t.Helper() + require := require.New(t) + rw := db.New(conn) + ctx := context.Background() + + a, err := NewAccount(ctx, am.ScopeId, am.PublicId, loginName, opt...) + require.NoError(err) + + id, err := newAccountId(ctx, am.PublicId, loginName) + require.NoError(err) + a.PublicId = id + + require.NoError(rw.Create(ctx, a)) + return a +} + +// TestManagedGroup creates a test ldap managed group. +func TestManagedGroup(t testing.TB, conn *db.DB, am *AuthMethod, grpNames []string, opt ...Option) *ManagedGroup { + t.Helper() + require := require.New(t) + rw := db.New(conn) + ctx := context.Background() + + mg, err := NewManagedGroup(ctx, am.PublicId, grpNames, opt...) + require.NoError(err) + + id, err := newManagedGroupId(ctx) + require.NoError(err) + mg.PublicId = id + + require.NoError(rw.Create(ctx, mg, db.WithLookup(true))) + return mg +} + +// TestGenerateCA will generate a test x509 CA cert, along with it encoded in a +// PEM format. +func TestGenerateCA(t testing.TB, hosts ...string) (*x509.Certificate, string) { + t.Helper() + require := require.New(t) + + priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + require.NoError(err) + + // ECDSA, ED25519 and RSA subject keys should have the DigitalSignature + // KeyUsage bits set in the x509.Certificate template + keyUsage := x509.KeyUsageDigitalSignature + + validFor := 2 * time.Minute + notBefore := time.Now() + notAfter := notBefore.Add(validFor) + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + require.NoError(err) + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"Acme Co"}, + }, + NotBefore: notBefore, + NotAfter: notAfter, + + KeyUsage: keyUsage, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + for _, h := range hosts { + if ip := net.ParseIP(h); ip != nil { + template.IPAddresses = append(template.IPAddresses, ip) + } else { + template.DNSNames = append(template.DNSNames, h) + } + } + + template.IsCA = true + template.KeyUsage |= x509.KeyUsageCertSign + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + require.NoError(err) + + c, err := x509.ParseCertificate(derBytes) + require.NoError(err) + + return c, string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})) +} + +// TestConvertToUrls will convert URL string representations to a slice of +// *url.URL +func TestConvertToUrls(t testing.TB, urls ...string) []*url.URL { + t.Helper() + require := require.New(t) + require.NotEmpty(urls) + var convertedUrls []*url.URL + for _, u := range urls { + parsed, err := url.Parse(u) + require.NoError(err) + require.Contains([]string{"ldap", "ldaps"}, parsed.Scheme) + convertedUrls = append(convertedUrls, parsed) + } + return convertedUrls +} + +// TestSortAuthMethods will sort the provided auth methods by public id and it +// will sort each auth method's embedded value objects +func TestSortAuthMethods(t testing.TB, methods []*AuthMethod) { + // sort them by public id first... + sort.Slice(methods, func(a, b int) bool { + return methods[a].PublicId < methods[b].PublicId + }) + + // sort all the embedded value objects... + for _, am := range methods { + sort.Slice(am.Urls, func(a, b int) bool { + return am.Urls[a] < am.Urls[b] + }) + sort.Slice(am.Certificates, func(a, b int) bool { + return am.Certificates[a] < am.Certificates[b] + }) + sort.Slice(am.AccountAttributeMaps, func(a, b int) bool { + return am.AccountAttributeMaps[a] < am.AccountAttributeMaps[b] + }) + } +} + +// TestGetAcctManagedGroups will retrieve the managed groups associated with an account. +func TestGetAcctManagedGroups(t testing.TB, conn *db.DB, acctId string) []string { + t.Helper() + testCtx := context.Background() + rw := db.New(conn) + var memberAccts []*grpMemberAccts + require.NoError(t, rw.SearchWhere(testCtx, &memberAccts, "member_id = ?", []any{acctId})) + grpIds := make([]string, 0, len(memberAccts)) + for _, mbrAcct := range memberAccts { + grpIds = append(grpIds, mbrAcct.ManagedGroupId) + } + return grpIds +} + +type grpMemberAccts struct { + CreateTime time.Time + MemberId string + ManagedGroupId string + ManagedGroupName string +} + +func (*grpMemberAccts) TableName() string { + return "auth_ldap_managed_group_member_account" +} diff --git a/internal/auth/ldap/testing_test.go b/internal/auth/ldap/testing_test.go new file mode 100644 index 0000000000..837fb2622d --- /dev/null +++ b/internal/auth/ldap/testing_test.go @@ -0,0 +1,121 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "crypto/rand" + "crypto/x509" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/crypto/ed25519" +) + +func Test_testAuthMethod(t *testing.T) { + t.Parallel() + assert, require := assert.New(t), require.New(t) + conn, _ := db.TestSetup(t, "postgres") + testWrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, conn, testWrapper) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, testWrapper)) + databaseWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + + testCtx := context.Background() + c1, c1Pem := TestGenerateCA(t, "localhost") + c2, c2Pem := TestGenerateCA(t, "127.0.0.1") + _, privKey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(err) + derPrivKey, err := x509.MarshalPKCS8PrivateKey(privKey) + require.NoError(err) + + am := TestAuthMethod( + t, conn, databaseWrapper, + org.PublicId, + []string{"ldaps://d1.alice.com", "ldap://d2.alice.com"}, + WithName(testCtx, "test-name"), + WithDescription(testCtx, "test-desc"), + WithStartTLS(testCtx), + WithInsecureTLS(testCtx), + WithDiscoverDn(testCtx), + WithAnonGroupSearch(testCtx), + WithUpnDomain(testCtx, "alice.com"), + WithCertificates(testCtx, c1, c2), + WithUserDn(testCtx, "user-dn"), + WithUserAttr(testCtx, "user-attr"), + WithUserFilter(testCtx, "user-filter"), + WithGroupDn(testCtx, "group-dn"), + WithGroupAttr(testCtx, "group-attr"), + WithGroupFilter(testCtx, "group-filter"), + WithClientCertificate(testCtx, derPrivKey, c2), // not a client cert, but good enough for the test. + WithBindCredential(testCtx, "bind-dn", "bind-password"), + ) + + rw := db.New(conn) + found := AllocAuthMethod() + found.PublicId = am.PublicId + rw.LookupById(testCtx, &found) + assert.Equal(am.PublicId, found.PublicId) + assert.Equal(found.ScopeId, org.PublicId) + assert.Equal(found.Name, "test-name") + assert.Equal(found.Description, "test-desc") + assert.Equal(found.OperationalState, InactiveState.String()) + assert.Equal(found.Version, uint32(1)) + assert.True(found.StartTls) + assert.True(found.InsecureTls) + assert.True(found.DiscoverDn) + assert.True(found.AnonGroupSearch) + assert.Equal("alice.com", found.UpnDomain) + + foundUrls := []*Url{} + err = rw.SearchWhere(testCtx, &foundUrls, "ldap_method_id = ?", []any{found.PublicId}, db.WithOrder("connection_priority asc")) + require.NoError(err) + assert.Equal("ldaps://d1.alice.com", foundUrls[0].GetServerUrl()) + assert.Equal(uint32(1), foundUrls[0].ConnectionPriority) + assert.Equal("ldap://d2.alice.com", foundUrls[1].GetServerUrl()) + assert.Equal(uint32(2), foundUrls[1].ConnectionPriority) + + foundCerts := []*Certificate{} + err = rw.SearchWhere(testCtx, &foundCerts, "ldap_method_id = ?", []any{found.PublicId}, db.WithOrder("create_time asc")) + require.NoError(err) + assert.Equal(c1Pem, foundCerts[0].GetCert()) + assert.Equal(c2Pem, foundCerts[1].GetCert()) + + foundUserSearchConf := allocUserEntrySearchConf() + err = rw.LookupWhere(testCtx, &foundUserSearchConf, "ldap_method_id = ?", []any{found.PublicId}) + require.NoError(err) + assert.Equal("user-dn", foundUserSearchConf.GetUserDn()) + assert.Equal("user-attr", foundUserSearchConf.GetUserAttr()) + assert.Equal("user-filter", foundUserSearchConf.GetUserFilter()) + + foundGroupSearchConf := allocGroupEntrySearchConf() + err = rw.LookupWhere(testCtx, &foundGroupSearchConf, "ldap_method_id = ?", []any{found.PublicId}) + require.NoError(err) + assert.Equal("group-dn", foundGroupSearchConf.GetGroupDn()) + assert.Equal("group-attr", foundGroupSearchConf.GetGroupAttr()) + assert.Equal("group-filter", foundGroupSearchConf.GetGroupFilter()) + + foundClientCert := allocClientCertificate() + err = rw.LookupWhere(testCtx, &foundClientCert, "ldap_method_id = ?", []any{found.PublicId}) + require.NoError(err) + require.NoError(foundClientCert.decrypt(testCtx, databaseWrapper)) + assert.NotEmpty(foundClientCert.GetKeyId()) + assert.NotEmpty(foundClientCert.GetCertificate()) + assert.NotEmpty(foundClientCert.GetCertificateKey()) + assert.NotEmpty(foundClientCert.GetCertificateKeyHmac()) + + foundBindCred := allocBindCredential() + err = rw.LookupWhere(testCtx, &foundBindCred, "ldap_method_id = ?", []any{found.PublicId}) + require.NoError(err) + require.NoError(foundBindCred.decrypt(testCtx, databaseWrapper)) + assert.NotEmpty(foundBindCred.GetKeyId()) + assert.NotEmpty(foundBindCred.GetDn()) + assert.NotEmpty(foundBindCred.GetPassword()) + assert.NotEmpty(foundBindCred.GetPasswordHmac()) +} diff --git a/internal/auth/ldap/url.go b/internal/auth/ldap/url.go new file mode 100644 index 0000000000..f94b9770e5 --- /dev/null +++ b/internal/auth/ldap/url.go @@ -0,0 +1,78 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "fmt" + "net/url" + + "github.com/hashicorp/boundary/internal/auth/ldap/store" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/go-secure-stdlib/strutil" + "google.golang.org/protobuf/proto" +) + +const urlTableName = "auth_ldap_url" + +// Url represents a required one to many auth method urls. It is assigned to an +// LDAP AuthMethod and updates/deletes to that AuthMethod are cascaded to its +// Urls. Urls are value objects of an AuthMethod, therefore there's no need for +// oplog metadata, since only the AuthMethod will have metadata because it's the +// root aggregate. +type Url struct { + *store.Url + tableName string +} + +// NewUrl creates a new in memory Url. connectionPriority cannot be less than +// one. No options are currently supported. +func NewUrl(ctx context.Context, authMethodId string, connectionPriority int, url *url.URL, _ ...Option) (*Url, error) { + const op = "ldap.NewUrl" + switch { + case authMethodId == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id") + case connectionPriority < 1: + return nil, errors.New(ctx, errors.InvalidParameter, op, "connection priority cannot be less than one") + case url == nil: + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing url") + case !strutil.StrListContainsCaseInsensitive([]string{"ldap", "ldaps"}, url.Scheme): + return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("scheme %q is not ldap or ldaps", url.Scheme)) + } + return &Url{ + Url: &store.Url{ + LdapMethodId: authMethodId, + ServerUrl: url.String(), + ConnectionPriority: uint32(connectionPriority), + }, + }, nil +} + +// allocUrl makes an empty one in memory +func allocUrl() *Url { + return &Url{ + Url: &store.Url{}, + } +} + +// clone a Url +func (u *Url) clone() *Url { + cp := proto.Clone(u.Url) + return &Url{ + Url: cp.(*store.Url), + } +} + +// TableName returns the table name +func (u *Url) TableName() string { + if u.tableName != "" { + return u.tableName + } + return urlTableName +} + +// SetTableName sets the table name. +func (u *Url) SetTableName(n string) { + u.tableName = n +} diff --git a/internal/auth/ldap/url_test.go b/internal/auth/ldap/url_test.go new file mode 100644 index 0000000000..afee454612 --- /dev/null +++ b/internal/auth/ldap/url_test.go @@ -0,0 +1,152 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "net/url" + "testing" + + "github.com/hashicorp/boundary/internal/auth/ldap/store" + "github.com/hashicorp/boundary/internal/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestNewUrl(t *testing.T) { + t.Parallel() + testCtx := context.Background() + tests := []struct { + name string + ctx context.Context + authMethodId string + priority int + url *url.URL + want *Url + wantErr bool + wantErrCode errors.Code + wantErrContains string + }{ + { + name: "valid", + ctx: testCtx, + authMethodId: "test-id", + priority: 10, + url: TestConvertToUrls(t, "ldaps://alice.com")[0], + want: &Url{ + Url: &store.Url{ + LdapMethodId: "test-id", + ConnectionPriority: 10, + ServerUrl: "ldaps://alice.com", + }, + }, + }, + { + name: "missing-auth-method-id", + ctx: testCtx, + url: TestConvertToUrls(t, "ldaps://alice.com")[0], + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "missing auth method id", + }, + { + name: "missing-url", + ctx: testCtx, + priority: 1, + authMethodId: "test-id", + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "missing url", + }, + { + name: "invalid-scheme", + ctx: testCtx, + authMethodId: "test-id", + priority: 1, + url: func() *url.URL { + parsed, err := url.Parse("https://alice.com") + require.NoError(t, err) + return parsed + }(), + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "scheme \"https\" is not ldap or ldaps", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := NewUrl(tc.ctx, tc.authMethodId, tc.priority, tc.url) + if tc.wantErr { + require.Error(err) + assert.Nil(got) + if tc.wantErrCode != errors.Unknown { + assert.True(errors.Match(errors.T(tc.wantErrCode), err)) + } + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.Equal(tc.want, got) + }) + } +} + +func TestUrl_SetTableName(t *testing.T) { + t.Parallel() + defaultTableName := urlTableName + tests := []struct { + name string + setNameTo string + want string + }{ + { + name: "new-name", + setNameTo: "new-name", + want: "new-name", + }, + { + name: "reset to default", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + def := allocUrl() + require.Equal(defaultTableName, def.TableName()) + m := allocUrl() + m.SetTableName(tt.setNameTo) + assert.Equal(tt.want, m.TableName()) + }) + } +} + +func TestUrl_clone(t *testing.T) { + t.Parallel() + testCtx := context.Background() + const priorityOfOne = 1 + t.Run("valid", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + u, err := NewUrl(testCtx, "test-id", priorityOfOne, TestConvertToUrls(t, "ldaps://alice.com")[0]) + require.NoError(err) + cp := u.clone() + assert.True(proto.Equal(cp.Url, u.Url)) + }) + t.Run("not-equal", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + u, err := NewUrl(testCtx, "test-id", priorityOfOne, TestConvertToUrls(t, "ldaps://alice.com")[0]) + require.NoError(err) + + u2, err := NewUrl(testCtx, "test-id", priorityOfOne, TestConvertToUrls(t, "ldaps://bob.com")[0]) + require.NoError(err) + + cp := u.clone() + assert.True(!proto.Equal(cp.Url, u2.Url)) + }) +} diff --git a/internal/auth/ldap/user_entry_search_conf.go b/internal/auth/ldap/user_entry_search_conf.go new file mode 100644 index 0000000000..703f654b16 --- /dev/null +++ b/internal/auth/ldap/user_entry_search_conf.go @@ -0,0 +1,78 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + + "github.com/hashicorp/boundary/internal/auth/ldap/store" + "github.com/hashicorp/boundary/internal/errors" + "google.golang.org/protobuf/proto" +) + +const userEntrySearchConfTableName = "auth_ldap_user_entry_search" + +// UserEntrySearchConf represent a set of optional configuration fields used to +// search for user entries. It is assigned to an LDAP AuthMethod and +// updates/deletes to that AuthMethod are cascaded to its UserEntrySearchConf. +// UserEntrySearchConf are value objects of an AuthMethod, therefore there's no +// need for oplog metadata, since only the AuthMethod will have metadata because +// it's the root aggregate. +type UserEntrySearchConf struct { + *store.UserEntrySearchConf + tableName string +} + +// NewUserEntrySearchConf creates a new in memory NewUserEntrySearchConf. +// Supported options are: WithUserDn, WithUserAttr, WithUserFilter and all other +// options are ignored. +func NewUserEntrySearchConf(ctx context.Context, authMethodId string, opt ...Option) (*UserEntrySearchConf, error) { + const op = "ldap.NewUserEntrySearchConf" + opts, err := getOpts(opt...) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + switch { + case authMethodId == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id") + case opts.withUserDn == "" && opts.withUserAttr == "" && opts.withUserFilter == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "you must supply either dn, attr, or filter") + } + return &UserEntrySearchConf{ + UserEntrySearchConf: &store.UserEntrySearchConf{ + LdapMethodId: authMethodId, + UserDn: opts.withUserDn, + UserAttr: opts.withUserAttr, + UserFilter: opts.withUserFilter, + }, + }, nil +} + +// allocUserEntrySearchConf makes an empty one in memory +func allocUserEntrySearchConf() *UserEntrySearchConf { + return &UserEntrySearchConf{ + UserEntrySearchConf: &store.UserEntrySearchConf{}, + } +} + +// clone a UserEntrySearchConf +func (uc *UserEntrySearchConf) clone() *UserEntrySearchConf { + cp := proto.Clone(uc.UserEntrySearchConf) + return &UserEntrySearchConf{ + UserEntrySearchConf: cp.(*store.UserEntrySearchConf), + } +} + +// TableName returns the table name +func (uc *UserEntrySearchConf) TableName() string { + if uc.tableName != "" { + return uc.tableName + } + return userEntrySearchConfTableName +} + +// SetTableName sets the table name. +func (uc *UserEntrySearchConf) SetTableName(n string) { + uc.tableName = n +} diff --git a/internal/auth/ldap/user_entry_search_conf_test.go b/internal/auth/ldap/user_entry_search_conf_test.go new file mode 100644 index 0000000000..4ec77d65fc --- /dev/null +++ b/internal/auth/ldap/user_entry_search_conf_test.go @@ -0,0 +1,180 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ldap + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/auth/ldap/store" + "github.com/hashicorp/boundary/internal/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestNewUserEntrySearchConf(t *testing.T) { + t.Parallel() + testCtx := context.Background() + tests := []struct { + name string + ctx context.Context + authMethodId string + opts []Option + want *UserEntrySearchConf + wantErr bool + wantErrCode errors.Code + wantErrContains string + }{ + { + name: "valid", + ctx: testCtx, + authMethodId: "test-id", + opts: []Option{ + WithUserDn(testCtx, "dn"), + WithUserAttr(testCtx, "attr"), + WithUserFilter(testCtx, "filter"), + }, + want: &UserEntrySearchConf{ + UserEntrySearchConf: &store.UserEntrySearchConf{ + LdapMethodId: "test-id", + UserDn: "dn", + UserAttr: "attr", + UserFilter: "filter", + }, + }, + }, + { + name: "just-dn", + ctx: testCtx, + authMethodId: "test-id", + opts: []Option{ + WithUserDn(testCtx, "dn"), + }, + want: &UserEntrySearchConf{ + UserEntrySearchConf: &store.UserEntrySearchConf{ + LdapMethodId: "test-id", + UserDn: "dn", + }, + }, + }, + { + name: "just-attr", + ctx: testCtx, + authMethodId: "test-id", + opts: []Option{ + WithUserAttr(testCtx, "attr"), + }, + want: &UserEntrySearchConf{ + UserEntrySearchConf: &store.UserEntrySearchConf{ + LdapMethodId: "test-id", + UserAttr: "attr", + }, + }, + }, + { + name: "just-filter", + ctx: testCtx, + authMethodId: "test-id", + opts: []Option{ + WithUserFilter(testCtx, "filter"), + }, + want: &UserEntrySearchConf{ + UserEntrySearchConf: &store.UserEntrySearchConf{ + LdapMethodId: "test-id", + UserFilter: "filter", + }, + }, + }, + { + name: "missing-auth-method-id", + ctx: testCtx, + opts: []Option{WithUserDn(testCtx, "dn")}, + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "missing auth method id", + }, + { + name: "no-opts", + ctx: testCtx, + authMethodId: "test-id", + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "you must supply either dn, attr, or filter", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := NewUserEntrySearchConf(tc.ctx, tc.authMethodId, tc.opts...) + if tc.wantErr { + require.Error(err) + assert.Nil(got) + if tc.wantErrCode != errors.Unknown { + assert.True(errors.Match(errors.T(tc.wantErrCode), err)) + } + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.Equal(tc.want, got) + }) + } +} + +func TestUserEntrySearchConf_SetTableName(t *testing.T) { + t.Parallel() + defaultTableName := userEntrySearchConfTableName + tests := []struct { + name string + setNameTo string + want string + }{ + { + name: "new-name", + setNameTo: "new-name", + want: "new-name", + }, + { + name: "reset to default", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + def := allocUserEntrySearchConf() + require.Equal(defaultTableName, def.TableName()) + m := allocUserEntrySearchConf() + m.SetTableName(tt.setNameTo) + assert.Equal(tt.want, m.TableName()) + }) + } +} + +func TestUserEntrySearchCon_clone(t *testing.T) { + t.Parallel() + testCtx := context.Background() + t.Run("valid", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + uc, err := NewUserEntrySearchConf(testCtx, "test-id", WithUserDn(testCtx, "dn")) + require.NoError(err) + cp := uc.clone() + assert.True(proto.Equal(cp.UserEntrySearchConf, uc.UserEntrySearchConf)) + }) + t.Run("not-equal", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + uc, err := NewUserEntrySearchConf(testCtx, "test-id", WithUserDn(testCtx, "dn")) + require.NoError(err) + + uc2, err := NewUserEntrySearchConf(testCtx, "test-id", WithUserDn(testCtx, "dn2")) + require.NoError(err) + + cp := uc.clone() + assert.True(!proto.Equal(cp.UserEntrySearchConf, uc2.UserEntrySearchConf)) + }) +} diff --git a/internal/auth/testing.go b/internal/auth/testing.go new file mode 100644 index 0000000000..ee770b3271 --- /dev/null +++ b/internal/auth/testing.go @@ -0,0 +1,58 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package auth + +import ( + "context" + "sort" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/stretchr/testify/require" +) + +// ManagedGroupMemberAccount represents an entry from +// auth_managed_group_member_account. These are used to determine the account +// ids where are a member of managed groups. See: oidc and ldap managed groups +// as well as iam role grants. +type ManagedGroupMemberAccount struct { + CreateTime *timestamp.Timestamp + MemberId string + ManagedGroupId string + tableName string +} + +// SetTableName sets the table name. +func (a *ManagedGroupMemberAccount) SetTableName(n string) { + a.tableName = n +} + +// TableName returns the table name. +func (a *ManagedGroupMemberAccount) TableName() string { + if a.tableName != "" { + return a.tableName + } + return "auth_managed_group_member_account" +} + +// TestSortManagedGroupMemberAccounts simply sorts them by public id to make +// comparisons a bit easier. +func TestSortManagedGroupMemberAccounts(t testing.TB, m []*ManagedGroupMemberAccount) { + sort.Slice(m, func(a, b int) bool { + return m[a].MemberId < m[b].MemberId + }) +} + +// TestManagedGroupMemberAccounts retrieves the accounts with membership in the +// specified managed group. +func TestManagedGroupMemberAccounts(t *testing.T, conn *db.DB, managedGroupId string) []*ManagedGroupMemberAccount { + var mgmAccts []*ManagedGroupMemberAccount + ctx := context.Background() + rw := db.New(conn) + err := rw.SearchWhere(ctx, &mgmAccts, "managed_group_id = ?", []any{managedGroupId}) + require.NoError(t, err) + TestSortManagedGroupMemberAccounts(t, mgmAccts) + return mgmAccts +} diff --git a/internal/cmd/commands.go b/internal/cmd/commands.go index b1daadc7d6..af68cec9de 100644 --- a/internal/cmd/commands.go +++ b/internal/cmd/commands.go @@ -74,6 +74,11 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Command: base.NewCommand(ui), }, nil }, + "authenticate ldap": func() (cli.Command, error) { + return &authenticate.LdapCommand{ + Command: base.NewCommand(ui), + }, nil + }, "accounts": func() (cli.Command, error) { return &accountscmd.Command{ @@ -128,6 +133,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "create", }, nil }, + "accounts create ldap": func() (cli.Command, error) { + return &accountscmd.LdapCommand{ + Command: base.NewCommand(ui), + Func: "create", + }, nil + }, "accounts update": func() (cli.Command, error) { return &accountscmd.Command{ Command: base.NewCommand(ui), @@ -146,6 +157,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "update", }, nil }, + "accounts update ldap": func() (cli.Command, error) { + return &accountscmd.LdapCommand{ + Command: base.NewCommand(ui), + Func: "update", + }, nil + }, "auth-methods": func() (cli.Command, error) { return &authmethodscmd.Command{ @@ -188,6 +205,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "create", }, nil }, + "auth-methods create ldap": func() (cli.Command, error) { + return &authmethodscmd.LdapCommand{ + Command: base.NewCommand(ui), + Func: "create", + }, nil + }, "auth-methods update": func() (cli.Command, error) { return &authmethodscmd.Command{ Command: base.NewCommand(ui), @@ -206,6 +229,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "update", }, nil }, + "auth-methods update ldap": func() (cli.Command, error) { + return &authmethodscmd.LdapCommand{ + Command: base.NewCommand(ui), + Func: "update", + }, nil + }, "auth-methods change-state oidc": func() (cli.Command, error) { return &authmethodscmd.OidcCommand{ Command: base.NewCommand(ui), @@ -817,6 +846,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "create", }, nil }, + "managed-groups create ldap": func() (cli.Command, error) { + return &managedgroupscmd.LdapCommand{ + Command: base.NewCommand(ui), + Func: "create", + }, nil + }, "managed-groups update": func() (cli.Command, error) { return &managedgroupscmd.Command{ Command: base.NewCommand(ui), @@ -829,6 +864,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "update", }, nil }, + "managed-groups update ldap": func() (cli.Command, error) { + return &managedgroupscmd.LdapCommand{ + Command: base.NewCommand(ui), + Func: "update", + }, nil + }, "roles": func() (cli.Command, error) { return &rolescmd.Command{ diff --git a/internal/cmd/commands/accountscmd/ldap_accounts.gen.go b/internal/cmd/commands/accountscmd/ldap_accounts.gen.go new file mode 100644 index 0000000000..c5d8b8546a --- /dev/null +++ b/internal/cmd/commands/accountscmd/ldap_accounts.gen.go @@ -0,0 +1,275 @@ +// Code generated by "make cli"; DO NOT EDIT. +package accountscmd + +import ( + "errors" + "fmt" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/accounts" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/boundary/internal/cmd/common" + "github.com/hashicorp/go-secure-stdlib/strutil" + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +func initLdapFlags() { + flagsOnce.Do(func() { + extraFlags := extraLdapActionsFlagsMapFunc() + for k, v := range extraFlags { + flagsLdapMap[k] = append(flagsLdapMap[k], v...) + } + }) +} + +var ( + _ cli.Command = (*LdapCommand)(nil) + _ cli.CommandAutocomplete = (*LdapCommand)(nil) +) + +type LdapCommand struct { + *base.Command + + Func string + + plural string + + extraLdapCmdVars +} + +func (c *LdapCommand) AutocompleteArgs() complete.Predictor { + initLdapFlags() + return complete.PredictAnything +} + +func (c *LdapCommand) AutocompleteFlags() complete.Flags { + initLdapFlags() + return c.Flags().Completions() +} + +func (c *LdapCommand) Synopsis() string { + if extra := extraLdapSynopsisFunc(c); extra != "" { + return extra + } + + synopsisStr := "account" + + synopsisStr = fmt.Sprintf("%s %s", "ldap-type", synopsisStr) + + return common.SynopsisFunc(c.Func, synopsisStr) +} + +func (c *LdapCommand) Help() string { + initLdapFlags() + + var helpStr string + helpMap := common.HelpMap("account") + + switch c.Func { + + default: + + helpStr = c.extraLdapHelpFunc(helpMap) + + } + + // Keep linter from complaining if we don't actually generate code using it + _ = helpMap + return helpStr +} + +var flagsLdapMap = map[string][]string{ + + "create": {"auth-method-id", "name", "description"}, + + "update": {"id", "name", "description", "version"}, +} + +func (c *LdapCommand) Flags() *base.FlagSets { + if len(flagsLdapMap[c.Func]) == 0 { + return c.FlagSet(base.FlagSetNone) + } + + set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat) + f := set.NewFlagSet("Command Options") + common.PopulateCommonFlags(c.Command, f, "ldap-type account", flagsLdapMap, c.Func) + + extraLdapFlagsFunc(c, set, f) + + return set +} + +func (c *LdapCommand) Run(args []string) int { + initLdapFlags() + + switch c.Func { + case "": + return cli.RunResultHelp + + } + + c.plural = "ldap-type account" + switch c.Func { + case "list": + c.plural = "ldap-type accounts" + } + + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + + if strutil.StrListContains(flagsLdapMap[c.Func], "id") && c.FlagId == "" { + c.PrintCliError(errors.New("ID is required but not passed in via -id")) + return base.CommandUserError + } + + var opts []accounts.Option + + if strutil.StrListContains(flagsLdapMap[c.Func], "auth-method-id") { + switch c.Func { + + case "create": + if c.FlagAuthMethodId == "" { + c.PrintCliError(errors.New("AuthMethod ID must be passed in via -auth-method-id or BOUNDARY_AUTH_METHOD_ID")) + return base.CommandUserError + } + + } + } + + client, err := c.Client() + if c.WrapperCleanupFunc != nil { + defer func() { + if err := c.WrapperCleanupFunc(); err != nil { + c.PrintCliError(fmt.Errorf("Error cleaning kms wrapper: %w", err)) + } + }() + } + if err != nil { + c.PrintCliError(fmt.Errorf("Error creating API client: %w", err)) + return base.CommandCliError + } + accountsClient := accounts.NewClient(client) + + switch c.FlagName { + case "": + case "null": + opts = append(opts, accounts.DefaultName()) + default: + opts = append(opts, accounts.WithName(c.FlagName)) + } + + switch c.FlagDescription { + case "": + case "null": + opts = append(opts, accounts.DefaultDescription()) + default: + opts = append(opts, accounts.WithDescription(c.FlagDescription)) + } + + if c.FlagFilter != "" { + opts = append(opts, accounts.WithFilter(c.FlagFilter)) + } + + var version uint32 + + switch c.Func { + + case "update": + switch c.FlagVersion { + case 0: + opts = append(opts, accounts.WithAutomaticVersioning(true)) + default: + version = uint32(c.FlagVersion) + } + + } + + if ok := extraLdapFlagsHandlingFunc(c, f, &opts); !ok { + return base.CommandUserError + } + + var resp *api.Response + var item *accounts.Account + + var createResult *accounts.AccountCreateResult + + var updateResult *accounts.AccountUpdateResult + + switch c.Func { + + case "create": + createResult, err = accountsClient.Create(c.Context, c.FlagAuthMethodId, opts...) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + resp = createResult.GetResponse() + item = createResult.GetItem() + + case "update": + updateResult, err = accountsClient.Update(c.Context, c.FlagId, version, opts...) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + resp = updateResult.GetResponse() + item = updateResult.GetItem() + + } + + resp, item, err = executeExtraLdapActions(c, resp, item, err, accountsClient, version, opts) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + + output, err := printCustomLdapActionOutput(c) + if err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + if output { + return base.CommandSuccess + } + + switch c.Func { + + } + + switch base.Format(c.UI) { + case "table": + c.UI.Output(printItemTable(item, resp)) + + case "json": + if ok := c.PrintJsonItem(resp); !ok { + return base.CommandCliError + } + } + + return base.CommandSuccess +} + +func (c *LdapCommand) checkFuncError(err error) int { + if err == nil { + return 0 + } + if apiErr := api.AsServerError(err); apiErr != nil { + c.PrintApiError(apiErr, fmt.Sprintf("Error from controller when performing %s on %s", c.Func, c.plural)) + return base.CommandApiError + } + c.PrintCliError(fmt.Errorf("Error trying to %s %s: %s", c.Func, c.plural, err.Error())) + return base.CommandCliError +} + +var ( + extraLdapActionsFlagsMapFunc = func() map[string][]string { return nil } + extraLdapSynopsisFunc = func(*LdapCommand) string { return "" } + extraLdapFlagsFunc = func(*LdapCommand, *base.FlagSets, *base.FlagSet) {} + extraLdapFlagsHandlingFunc = func(*LdapCommand, *base.FlagSets, *[]accounts.Option) bool { return true } + executeExtraLdapActions = func(_ *LdapCommand, inResp *api.Response, inItem *accounts.Account, inErr error, _ *accounts.Client, _ uint32, _ []accounts.Option) (*api.Response, *accounts.Account, error) { + return inResp, inItem, inErr + } + printCustomLdapActionOutput = func(*LdapCommand) (bool, error) { return false, nil } +) diff --git a/internal/cmd/commands/accountscmd/ldap_funcs.go b/internal/cmd/commands/accountscmd/ldap_funcs.go new file mode 100644 index 0000000000..82f19cf232 --- /dev/null +++ b/internal/cmd/commands/accountscmd/ldap_funcs.go @@ -0,0 +1,89 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package accountscmd + +import ( + "github.com/hashicorp/boundary/api/accounts" + "github.com/hashicorp/boundary/internal/cmd/base" +) + +const ( + loginNameFlagName = "login-name" +) + +func init() { + extraLdapActionsFlagsMapFunc = extraLdapActionsFlagsMapFuncImpl + extraLdapFlagsFunc = extraLdapFlagsFuncImpl + extraLdapFlagsHandlingFunc = extraLdapFlagsHandlingFuncImpl +} + +func extraLdapActionsFlagsMapFuncImpl() map[string][]string { + return map[string][]string{ + "create": {loginNameFlagName}, + } +} + +type extraLdapCmdVars struct { + flagLoginName string +} + +func (c *LdapCommand) extraLdapHelpFunc(helpMap map[string]func() string) string { + var helpStr string + switch c.Func { + case "create": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary accounts create ldap [options] [args]", + "", + " Create an ldap-type account. Example:", + "", + ` $ boundary accounts create ldap -login-name prodops -description "ldap account for ProdOps"`, + "", + "", + }) + + case "update": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary accounts update ldap [options] [args]", + "", + " Update an ldap-type account given its ID. Example:", + "", + ` $ boundary accounts update ldap -id acctldap_1234567890 -name "devops" -description "ldap account for DevOps"`, + "", + "", + }) + } + return helpStr + c.Flags().Help() +} + +func extraLdapFlagsFuncImpl(c *LdapCommand, set *base.FlagSets, _ *base.FlagSet) { + f := set.NewFlagSet("Ldap Account Options") + + for _, name := range flagsLdapMap[c.Func] { + switch name { + case loginNameFlagName: + f.StringVar(&base.StringVar{ + Name: loginNameFlagName, + Target: &c.flagLoginName, + Usage: "The login name for the account.", + }) + } + } +} + +func extraLdapFlagsHandlingFuncImpl(c *LdapCommand, _ *base.FlagSets, opts *[]accounts.Option) bool { + switch c.flagLoginName { + case "null", "": + if c.Func == "create" { + c.UI.Error("Login-name must be passed in via -login-name") + return false + } + default: + if c.Func != "create" { + c.UI.Error("-login-name can only be set when creating an ldap account") + return false + } + *opts = append(*opts, accounts.WithLdapAccountLoginName(c.flagLoginName)) + } + return true +} diff --git a/internal/cmd/commands/authenticate/authenticate.go b/internal/cmd/commands/authenticate/authenticate.go index aac1a7712b..41335e564d 100644 --- a/internal/cmd/commands/authenticate/authenticate.go +++ b/internal/cmd/commands/authenticate/authenticate.go @@ -49,6 +49,10 @@ func (c *Command) Help() string { "", " $ boundary authenticate oidc -auth-method-id amoidc_1234567890", "", + " Authenticate with an LDAP auth method:", + "", + " $ boundary authenticate ldap -auth-method-id amldap_1234567890", + "", " Please see the auth method subcommand help for detailed usage information.", }) + c.Flags().Help() } @@ -107,8 +111,12 @@ func (c *Command) Run(args []string) int { cmd := OidcCommand{Command: c.Command, Opts: []common.Option{common.WithSkipScopeIdFlag(true)}} cmd.Run([]string{}) + case strings.HasPrefix(c.FlagAuthMethodId, globals.LdapAuthMethodPrefix): + cmd := LdapCommand{Command: c.Command, Opts: []common.Option{common.WithSkipScopeIdFlag(true)}} + cmd.Run([]string{}) + default: - c.PrintCliError(fmt.Errorf("The primary auth method was of an unsupported type. The given ID was %s; only 'ampw' (password) and 'amoidc' (OIDC) auth method prefixes are supported.", c.FlagAuthMethodId)) + c.PrintCliError(fmt.Errorf("The primary auth method was of an unsupported type. The given ID was %s; only 'ampw' (password), 'amoidc' (OIDC) and 'amldap' (LDAP) auth method prefixes are supported.", c.FlagAuthMethodId)) return cli.RunResultHelp } diff --git a/internal/cmd/commands/authenticate/ldap.go b/internal/cmd/commands/authenticate/ldap.go new file mode 100644 index 0000000000..d2071451ae --- /dev/null +++ b/internal/cmd/commands/authenticate/ldap.go @@ -0,0 +1,197 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package authenticate + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/authmethods" + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/boundary/internal/cmd/common" + "github.com/hashicorp/boundary/internal/types/scope" + "github.com/hashicorp/go-secure-stdlib/parseutil" + "github.com/hashicorp/go-secure-stdlib/password" + "github.com/mitchellh/cli" + "github.com/mitchellh/go-wordwrap" + "github.com/posener/complete" +) + +var ( + _ cli.Command = (*PasswordCommand)(nil) + _ cli.CommandAutocomplete = (*PasswordCommand)(nil) +) + +type LdapCommand struct { + *base.Command + + flagLoginName string + flagPassword string + Opts []common.Option + parsedOpts *common.Options +} + +func (c *LdapCommand) Synopsis() string { + return wordwrap.WrapString("Invoke the ldap auth method to authenticate with Boundary", base.TermWidth) +} + +func (c *LdapCommand) Help() string { + return base.WrapForHelpText([]string{ + "Usage: boundary authenticate ldap [options] [args]", + "", + " Invoke the ldap auth method to authenticate the Boundary CLI. Example:", + "", + ` $ boundary authenticate ldap -auth-method-id amldap_1234567890 -login-name foo`, + "", + "", + }) + c.Flags().Help() +} + +func (c *LdapCommand) Flags() *base.FlagSets { + set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat) + f := set.NewFlagSet("Command Options") + + f.StringVar(&base.StringVar{ + Name: "login-name", + Target: &c.flagLoginName, + EnvVar: envLoginName, + Usage: "The login name corresponding to an account within the given auth method.", + }) + + f.StringVar(&base.StringVar{ + Name: "password", + Target: &c.flagPassword, + EnvVar: envPassword, + Usage: "The password associated with the login name. If blank, the command will prompt for the password to be entered interactively in a non-echoing way. Otherwise, this can refer to a file on disk (file://) from which a password will be read or an env var (env://) from which the password will be read.", + }) + + f.StringVar(&base.StringVar{ + Name: "auth-method-id", + EnvVar: "BOUNDARY_AUTH_METHOD_ID", + Target: &c.FlagAuthMethodId, + Usage: "The auth-method resource to use for the operation.", + }) + + if c.parsedOpts == nil || !c.parsedOpts.WithSkipScopeIdFlag { + f.StringVar(&base.StringVar{ + Name: "scope-id", + EnvVar: "BOUNDARY_SCOPE_ID", + Target: &c.FlagScopeId, + Usage: "The scope ID to use for the operation.", + }) + } + + return set +} + +func (c *LdapCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictAnything +} + +func (c *LdapCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *LdapCommand) Run(args []string) int { + opts, err := common.GetOpts(c.Opts...) + if err != nil { + c.PrintCliError(err) + return base.CommandCliError + } + c.parsedOpts = opts + + f := c.Flags() + if err := f.Parse(args); err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + + switch c.flagLoginName { + case "": + var input string + fmt.Print("Please enter the login name: ") + _, err := fmt.Scanln(&input) + if err != nil { + c.UI.Error(fmt.Sprintf("An error occurred attempting to read the login name. The raw error message is shown below but usually this is because you attempted to pipe a value into the command or you are executing outside of a terminal (TTY). The raw error was:\n\n%s", err.Error())) + return base.CommandUserError + } + c.flagLoginName = strings.TrimSpace(input) + } + + switch c.flagPassword { + case "": + fmt.Print("Please enter the password (it will be hidden): ") + value, err := password.Read(os.Stdin) + fmt.Print("\n") + if err != nil { + c.UI.Error(fmt.Sprintf("An error occurred attempting to read the password. The raw error message is shown below but usually this is because you attempted to pipe a value into the command or you are executing outside of a terminal (TTY). The raw error was:\n\n%s", err.Error())) + return base.CommandUserError + } + c.flagPassword = strings.TrimSpace(value) + + default: + password, err := parseutil.MustParsePath(c.flagPassword) + switch { + case err == nil: + case errors.Is(err, parseutil.ErrNotParsed): + c.UI.Error("Password flag must be used with env:// or file:// syntax or left empty for an interactive prompt") + return base.CommandUserError + default: + c.UI.Error(fmt.Sprintf("Error parsing password flag: %v", err)) + return base.CommandUserError + } + c.flagPassword = password + } + + client, err := c.Client(base.WithNoTokenScope(), base.WithNoTokenValue()) + if c.WrapperCleanupFunc != nil { + defer func() { + if err := c.WrapperCleanupFunc(); err != nil { + c.PrintCliError(fmt.Errorf("Error cleaning kms wrapper: %w", err)) + } + }() + } + if err != nil { + c.PrintCliError(fmt.Errorf("Error creating API client: %w", err)) + return base.CommandCliError + } + + aClient := authmethods.NewClient(client) + + // if auth method ID isn't passed on the CLI, try looking up the primary auth method ID + if c.FlagAuthMethodId == "" { + // if flag for scope is empty try looking up global + if c.FlagScopeId == "" { + c.FlagScopeId = scope.Global.String() + } + + pri, err := getPrimaryAuthMethodId(c.Context, aClient, c.FlagScopeId, globals.LdapAuthMethodPrefix) + if err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + + c.FlagAuthMethodId = pri + } + + result, err := aClient.Authenticate(c.Context, c.FlagAuthMethodId, "login", + map[string]any{ + "login_name": c.flagLoginName, + "password": c.flagPassword, + }) + if err != nil { + if apiErr := api.AsServerError(err); apiErr != nil { + c.PrintApiError(apiErr, "Error from controller when performing authentication") + return base.CommandApiError + } + c.PrintCliError(fmt.Errorf("Error trying to perform authentication: %w", err)) + return base.CommandCliError + } + + return saveAndOrPrintToken(c.Command, result) +} diff --git a/internal/cmd/commands/authmethodscmd/ldap_authmethods.gen.go b/internal/cmd/commands/authmethodscmd/ldap_authmethods.gen.go new file mode 100644 index 0000000000..852ccbeef5 --- /dev/null +++ b/internal/cmd/commands/authmethodscmd/ldap_authmethods.gen.go @@ -0,0 +1,280 @@ +// Code generated by "make cli"; DO NOT EDIT. +package authmethodscmd + +import ( + "errors" + "fmt" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/authmethods" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/boundary/internal/cmd/common" + "github.com/hashicorp/go-secure-stdlib/strutil" + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +func initLdapFlags() { + flagsOnce.Do(func() { + extraFlags := extraLdapActionsFlagsMapFunc() + for k, v := range extraFlags { + flagsLdapMap[k] = append(flagsLdapMap[k], v...) + } + }) +} + +var ( + _ cli.Command = (*LdapCommand)(nil) + _ cli.CommandAutocomplete = (*LdapCommand)(nil) +) + +type LdapCommand struct { + *base.Command + + Func string + + plural string + + extraLdapCmdVars +} + +func (c *LdapCommand) AutocompleteArgs() complete.Predictor { + initLdapFlags() + return complete.PredictAnything +} + +func (c *LdapCommand) AutocompleteFlags() complete.Flags { + initLdapFlags() + return c.Flags().Completions() +} + +func (c *LdapCommand) Synopsis() string { + if extra := extraLdapSynopsisFunc(c); extra != "" { + return extra + } + + synopsisStr := "auth method" + + synopsisStr = fmt.Sprintf("%s %s", "ldap-type", synopsisStr) + + return common.SynopsisFunc(c.Func, synopsisStr) +} + +func (c *LdapCommand) Help() string { + initLdapFlags() + + var helpStr string + helpMap := common.HelpMap("auth method") + + switch c.Func { + + default: + + helpStr = c.extraLdapHelpFunc(helpMap) + + } + + // Keep linter from complaining if we don't actually generate code using it + _ = helpMap + return helpStr +} + +var flagsLdapMap = map[string][]string{ + + "create": {"scope-id", "name", "description"}, + + "update": {"id", "name", "description", "version"}, +} + +func (c *LdapCommand) Flags() *base.FlagSets { + if len(flagsLdapMap[c.Func]) == 0 { + return c.FlagSet(base.FlagSetNone) + } + + set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat) + f := set.NewFlagSet("Command Options") + common.PopulateCommonFlags(c.Command, f, "ldap-type auth method", flagsLdapMap, c.Func) + + extraLdapFlagsFunc(c, set, f) + + return set +} + +func (c *LdapCommand) Run(args []string) int { + initLdapFlags() + + switch c.Func { + case "": + return cli.RunResultHelp + + } + + c.plural = "ldap-type auth method" + switch c.Func { + case "list": + c.plural = "ldap-type auth methods" + } + + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + + if strutil.StrListContains(flagsLdapMap[c.Func], "id") && c.FlagId == "" { + c.PrintCliError(errors.New("ID is required but not passed in via -id")) + return base.CommandUserError + } + + var opts []authmethods.Option + + if strutil.StrListContains(flagsLdapMap[c.Func], "scope-id") { + switch c.Func { + + case "create": + if c.FlagScopeId == "" { + c.PrintCliError(errors.New("Scope ID must be passed in via -scope-id or BOUNDARY_SCOPE_ID")) + return base.CommandUserError + } + + } + } + + client, err := c.Client() + if c.WrapperCleanupFunc != nil { + defer func() { + if err := c.WrapperCleanupFunc(); err != nil { + c.PrintCliError(fmt.Errorf("Error cleaning kms wrapper: %w", err)) + } + }() + } + if err != nil { + c.PrintCliError(fmt.Errorf("Error creating API client: %w", err)) + return base.CommandCliError + } + authmethodsClient := authmethods.NewClient(client) + + switch c.FlagName { + case "": + case "null": + opts = append(opts, authmethods.DefaultName()) + default: + opts = append(opts, authmethods.WithName(c.FlagName)) + } + + switch c.FlagDescription { + case "": + case "null": + opts = append(opts, authmethods.DefaultDescription()) + default: + opts = append(opts, authmethods.WithDescription(c.FlagDescription)) + } + + switch c.FlagRecursive { + case true: + opts = append(opts, authmethods.WithRecursive(true)) + } + + if c.FlagFilter != "" { + opts = append(opts, authmethods.WithFilter(c.FlagFilter)) + } + + var version uint32 + + switch c.Func { + + case "update": + switch c.FlagVersion { + case 0: + opts = append(opts, authmethods.WithAutomaticVersioning(true)) + default: + version = uint32(c.FlagVersion) + } + + } + + if ok := extraLdapFlagsHandlingFunc(c, f, &opts); !ok { + return base.CommandUserError + } + + var resp *api.Response + var item *authmethods.AuthMethod + + var createResult *authmethods.AuthMethodCreateResult + + var updateResult *authmethods.AuthMethodUpdateResult + + switch c.Func { + + case "create": + createResult, err = authmethodsClient.Create(c.Context, "ldap", c.FlagScopeId, opts...) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + resp = createResult.GetResponse() + item = createResult.GetItem() + + case "update": + updateResult, err = authmethodsClient.Update(c.Context, c.FlagId, version, opts...) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + resp = updateResult.GetResponse() + item = updateResult.GetItem() + + } + + resp, item, err = executeExtraLdapActions(c, resp, item, err, authmethodsClient, version, opts) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + + output, err := printCustomLdapActionOutput(c) + if err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + if output { + return base.CommandSuccess + } + + switch c.Func { + + } + + switch base.Format(c.UI) { + case "table": + c.UI.Output(printItemTable(item, resp)) + + case "json": + if ok := c.PrintJsonItem(resp); !ok { + return base.CommandCliError + } + } + + return base.CommandSuccess +} + +func (c *LdapCommand) checkFuncError(err error) int { + if err == nil { + return 0 + } + if apiErr := api.AsServerError(err); apiErr != nil { + c.PrintApiError(apiErr, fmt.Sprintf("Error from controller when performing %s on %s", c.Func, c.plural)) + return base.CommandApiError + } + c.PrintCliError(fmt.Errorf("Error trying to %s %s: %s", c.Func, c.plural, err.Error())) + return base.CommandCliError +} + +var ( + extraLdapActionsFlagsMapFunc = func() map[string][]string { return nil } + extraLdapSynopsisFunc = func(*LdapCommand) string { return "" } + extraLdapFlagsFunc = func(*LdapCommand, *base.FlagSets, *base.FlagSet) {} + extraLdapFlagsHandlingFunc = func(*LdapCommand, *base.FlagSets, *[]authmethods.Option) bool { return true } + executeExtraLdapActions = func(_ *LdapCommand, inResp *api.Response, inItem *authmethods.AuthMethod, inErr error, _ *authmethods.Client, _ uint32, _ []authmethods.Option) (*api.Response, *authmethods.AuthMethod, error) { + return inResp, inItem, inErr + } + printCustomLdapActionOutput = func(*LdapCommand) (bool, error) { return false, nil } +) diff --git a/internal/cmd/commands/authmethodscmd/ldap_funcs.go b/internal/cmd/commands/authmethodscmd/ldap_funcs.go new file mode 100644 index 0000000000..403cacc9a6 --- /dev/null +++ b/internal/cmd/commands/authmethodscmd/ldap_funcs.go @@ -0,0 +1,510 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package authmethodscmd + +import ( + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "net/url" + + "github.com/hashicorp/boundary/api/authmethods" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/go-secure-stdlib/parseutil" +) + +func init() { + extraLdapActionsFlagsMapFunc = extraLdapActionsFlagsMapFuncImpl + extraLdapFlagsFunc = extraLdapFlagsFuncImpl + extraLdapFlagsHandlingFunc = extraLdapFlagHandlingFuncImpl +} + +type extraLdapCmdVars struct { + flagState string + flagUrls []string + flagInsecureTls bool + flagDiscoverDn bool + flagAnonGroupSearch bool + flagUpnDomain string + flagStartTls bool + flagUserDn string + flagUserAttr string + flagUserFilter string + flagEnableGroups bool + flagGroupDn string + flagGroupAttr string + flagGroupFilter string + flagCertificates []string + flagClientCertificate string + flagClientCertificateKey string + flagBindDn string + flagBindPassword string + flagUseTokenGroups bool + flagAccountAttributeMaps []string +} + +const ( + urlsFlagName = "urls" + insecureTlsFlagName = "insecure-tls" + discoverDnFlagName = "discover-dn" + anonGroupSearchFlagName = "anon-group-search" + upnDomainFlagName = "upn-domain" + startTlsFlagName = "start-tls" + userDnFlagName = "user-dn" + userAttrFlagName = "user-attr" + userFilterFlagName = "user-filter" + enableGroupsFlagName = "enable-groups" + groupDnFlagName = "group-dn" + groupAttrFlagName = "group-attr" + groupFilterFlagName = "group-filter" + certificatesFlagName = "certificate" + clientCertificateFlagName = "client-certificate" + clientCertificateKeyFlagName = "client-certificate-key" + bindDnFlagName = "bind-dn" + bindPasswordFlagName = "bind-password" + useTokenGroupsFlagName = "use-token-groups" + accountAttributeMaps = "account-attribute-map" +) + +func extraLdapActionsFlagsMapFuncImpl() map[string][]string { + flags := map[string][]string{ + "create": { + urlsFlagName, + insecureTlsFlagName, + discoverDnFlagName, + anonGroupSearchFlagName, + upnDomainFlagName, + startTlsFlagName, + userDnFlagName, + userAttrFlagName, + userFilterFlagName, + enableGroupsFlagName, + groupDnFlagName, + groupAttrFlagName, + groupFilterFlagName, + certificatesFlagName, + clientCertificateFlagName, + clientCertificateKeyFlagName, + bindDnFlagName, + bindPasswordFlagName, + useTokenGroupsFlagName, + accountAttributeMaps, + stateFlagName, + }, + } + flags["update"] = flags["create"] + return flags +} + +func extraLdapFlagsFuncImpl(c *LdapCommand, set *base.FlagSets, _ *base.FlagSet) { + f := set.NewFlagSet("LDAP Auth Method Options") + + for _, name := range flagsLdapMap[c.Func] { + switch name { + case urlsFlagName: + f.StringSliceVar(&base.StringSliceVar{ + Name: urlsFlagName, + Target: &c.flagUrls, + Usage: "The LDAP URLs that specify LDAP servers to connect to (required). May be specified multiple times.", + }) + case startTlsFlagName: + f.BoolVar(&base.BoolVar{ + Name: startTlsFlagName, + Target: &c.flagStartTls, + Usage: "Issue StartTLS command after connecting (optional).", + }) + case insecureTlsFlagName: + f.BoolVar(&base.BoolVar{ + Name: insecureTlsFlagName, + Target: &c.flagInsecureTls, + Usage: "Skip the LDAP server SSL certificate validation (optional) - insecure and use with caution.", + }) + case discoverDnFlagName: + f.BoolVar(&base.BoolVar{ + Name: discoverDnFlagName, + Target: &c.flagDiscoverDn, + Usage: "Use anon bind to discover the bind DN of a user (optional).", + }) + case anonGroupSearchFlagName: + f.BoolVar(&base.BoolVar{ + Name: anonGroupSearchFlagName, + Target: &c.flagAnonGroupSearch, + Usage: "Use anon bind when performing LDAP group searches (optional).", + }) + case upnDomainFlagName: + f.StringVar(&base.StringVar{ + Name: upnDomainFlagName, + Target: &c.flagUpnDomain, + Usage: "The userPrincipalDomain used to construct the UPN string for the authenticating user (optional).", + }) + case userDnFlagName: + f.StringVar(&base.StringVar{ + Name: userDnFlagName, + Target: &c.flagUserDn, + Usage: "The base DN under which to perform user search (optional).", + }) + case userAttrFlagName: + f.StringVar(&base.StringVar{ + Name: userAttrFlagName, + Target: &c.flagUserAttr, + Usage: "The attribute on user entry matching the username passed when authenticating (optional).", + }) + case userFilterFlagName: + f.StringVar(&base.StringVar{ + Name: userFilterFlagName, + Target: &c.flagUserFilter, + Usage: "A go template used to construct a LDAP user search filter (optional).", + }) + case enableGroupsFlagName: + f.BoolVar(&base.BoolVar{ + Name: enableGroupsFlagName, + Target: &c.flagEnableGroups, + Usage: "Find the authenticated user's groups during authentication (optional).", + }) + case groupDnFlagName: + f.StringVar(&base.StringVar{ + Name: groupDnFlagName, + Target: &c.flagGroupDn, + Usage: "The base DN under which to perform group search.", + }) + case groupAttrFlagName: + f.StringVar(&base.StringVar{ + Name: groupAttrFlagName, + Target: &c.flagGroupAttr, + Usage: "The attribute that enumerates a user's group membership from entries returned by a group search (optional).", + }) + case groupFilterFlagName: + f.StringVar(&base.StringVar{ + Name: groupFilterFlagName, + Target: &c.flagGroupFilter, + Usage: "A go template used to construct a LDAP group search filter (optional).", + }) + case certificatesFlagName: + f.StringSliceVar(&base.StringSliceVar{ + Name: certificatesFlagName, + Target: &c.flagCertificates, + Usage: "PEM-encoded X.509 CA certificate in ASN.1 DER form that can be used as a trust anchor when connecting to an LDAP server(optional). This may be specified multiple times", + }) + case clientCertificateFlagName: + f.StringVar(&base.StringVar{ + Name: clientCertificateFlagName, + Target: &c.flagClientCertificate, + Usage: "PEM-encoded X.509 client certificate in ASN.1 DER form that can be used to authenticate against an LDAP server(optional).", + }) + case clientCertificateKeyFlagName: + f.StringVar(&base.StringVar{ + Name: clientCertificateKeyFlagName, + Target: &c.flagClientCertificateKey, + Usage: "PEM-encoded X.509 client certificate key in PKCS #8, ASN.1 DER form used with the client certificate (optional).", + }) + case bindDnFlagName: + f.StringVar(&base.StringVar{ + Name: bindDnFlagName, + Target: &c.flagBindDn, + Usage: "The distinguished name of entry to bind when performing user and group searches (optional).", + }) + case bindPasswordFlagName: + f.StringVar(&base.StringVar{ + Name: bindPasswordFlagName, + Target: &c.flagBindPassword, + Usage: "The password to use along with bind-dn performing user and group searches (optional).", + }) + case useTokenGroupsFlagName: + f.BoolVar(&base.BoolVar{ + Name: useTokenGroupsFlagName, + Target: &c.flagUseTokenGroups, + Usage: "Use the Active Directory tokenGroups constructed attribute of the user to find the group memberships (optional).", + }) + case stateFlagName: + f.StringVar(&base.StringVar{ + Name: stateFlagName, + Target: &c.flagState, + Usage: "The desired operational state of the auth method.", + }) + } + } +} + +func (c *LdapCommand) extraLdapHelpFunc(helpMap map[string]func() string) string { + var helpStr string + switch c.Func { + case "create": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary auth-methods create ldap [options] [args]", + "", + " Create an ldap-type auth method. Example:", + "", + ` $ boundary auth-methods create ldap -name prodops -description "LDAP auth-method for ProdOps"`, + "", + "", + }) + + case "update": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary auth-methods update ldap [options] [args]", + "", + " Update an ldap-type auth method given its ID. Example:", + "", + ` $ boundary auth-methods update ldap -id amldap_1234567890 -name "devops" -description "LDAP auth-method for DevOps"`, + "", + "", + }) + } + return helpStr + c.Flags().Help() +} + +func extraLdapFlagHandlingFuncImpl(c *LdapCommand, _ *base.FlagSets, opts *[]authmethods.Option) bool { + switch { + case len(c.flagUrls) == 0: + case len(c.flagUrls) == 1 && c.flagUrls[0] == "null": + c.UI.Error(fmt.Sprintf("There must be at least one %q", urlsFlagName)) + return false + default: + for _, urlString := range c.flagUrls { + u, err := url.Parse(urlString) + if err != nil { + c.UI.Error(fmt.Sprintf("Error parsing URL %q: %s", urlString, err)) + return false + } + switch u.Scheme { + case "ldap", "ldaps": + default: + c.UI.Error(fmt.Sprintf("scheme in url %q is neither ldap nor ldaps", urlString)) + return false + } + } + *opts = append(*opts, authmethods.WithLdapAuthMethodUrls(c.flagUrls)) + } + + switch c.flagStartTls { + case true: + *opts = append(*opts, authmethods.WithLdapAuthMethodStartTls(true)) + default: + *opts = append(*opts, authmethods.WithLdapAuthMethodStartTls(false)) + } + + switch c.flagInsecureTls { + case true: + *opts = append(*opts, authmethods.WithLdapAuthMethodInsecureTls(true)) + default: + *opts = append(*opts, authmethods.WithLdapAuthMethodInsecureTls(false)) + } + + switch c.flagDiscoverDn { + case true: + *opts = append(*opts, authmethods.WithLdapAuthMethodDiscoverDn(true)) + default: + *opts = append(*opts, authmethods.WithLdapAuthMethodDiscoverDn(false)) + } + + switch c.flagAnonGroupSearch { + case true: + *opts = append(*opts, authmethods.WithLdapAuthMethodAnonGroupSearch(true)) + default: + *opts = append(*opts, authmethods.WithLdapAuthMethodAnonGroupSearch(false)) + } + + switch c.flagUpnDomain { + case "": + case "null": + *opts = append(*opts, authmethods.DefaultLdapAuthMethodUpnDomain()) + default: + *opts = append(*opts, authmethods.WithLdapAuthMethodUpnDomain(c.flagUpnDomain)) + } + + switch c.flagUserDn { + case "": + case "null": + *opts = append(*opts, authmethods.DefaultLdapAuthMethodUserDn()) + default: + *opts = append(*opts, authmethods.WithLdapAuthMethodUserDn(c.flagUserDn)) + } + + switch c.flagUserAttr { + case "": + case "null": + *opts = append(*opts, authmethods.DefaultLdapAuthMethodUserAttr()) + default: + *opts = append(*opts, authmethods.WithLdapAuthMethodUserAttr(c.flagUserAttr)) + } + + switch c.flagUserFilter { + case "": + case "null": + *opts = append(*opts, authmethods.DefaultLdapAuthMethodUserFilter()) + default: + *opts = append(*opts, authmethods.WithLdapAuthMethodUserFilter(c.flagUserFilter)) + } + + switch c.flagEnableGroups { + case true: + *opts = append(*opts, authmethods.WithLdapAuthMethodEnableGroups(true)) + default: + *opts = append(*opts, authmethods.WithLdapAuthMethodEnableGroups(false)) + } + + switch c.flagGroupDn { + case "": + case "null": + *opts = append(*opts, authmethods.DefaultLdapAuthMethodGroupDn()) + default: + *opts = append(*opts, authmethods.WithLdapAuthMethodGroupDn(c.flagGroupDn)) + } + + switch c.flagGroupAttr { + case "": + case "null": + *opts = append(*opts, authmethods.DefaultLdapAuthMethodGroupAttr()) + default: + *opts = append(*opts, authmethods.WithLdapAuthMethodGroupAttr(c.flagGroupAttr)) + } + + switch c.flagGroupFilter { + case "": + case "null": + *opts = append(*opts, authmethods.DefaultLdapAuthMethodGroupFilter()) + default: + *opts = append(*opts, authmethods.WithLdapAuthMethodGroupFilter(c.flagGroupFilter)) + } + + switch { + case len(c.flagCertificates) == 0: + case len(c.flagCertificates) == 1 && c.flagCertificates[0] == "null": + *opts = append(*opts, authmethods.DefaultLdapAuthMethodCertificates()) + default: + pems := make([]string, 0, len(c.flagCertificates)) + for _, certFlag := range c.flagCertificates { + p, err := parseutil.MustParsePath(certFlag) + switch { + case err == nil: + if validationErr := validateCerts(p); validationErr != nil { + c.UI.Error(fmt.Sprintf("invalid certificate in %q: %s", certFlag, validationErr.Error())) + return false + } + pems = append(pems, p) + case errors.Is(err, parseutil.ErrNotParsed): + c.UI.Error("Certificate flag must be used with env:// or file:// syntax") + return false + default: + c.UI.Error(fmt.Sprintf("Error parsing certificate flag: %v", err)) + return false + } + } + *opts = append(*opts, authmethods.WithLdapAuthMethodCertificates(pems)) + } + + switch c.flagClientCertificate { + case "": + case "null": + *opts = append(*opts, authmethods.DefaultLdapAuthMethodClientCertificate()) + default: + p, err := parseutil.MustParsePath(c.flagClientCertificate) + switch { + case err == nil: + if validationErr := validateCerts(p); validationErr != nil { + c.UI.Error(fmt.Sprintf("invalid client certificate in %q: %s", c.flagClientCertificate, validationErr.Error())) + return false + } + *opts = append(*opts, authmethods.WithLdapAuthMethodClientCertificate(p)) + case errors.Is(err, parseutil.ErrNotParsed): + c.UI.Error("Client certificate flag must be used with env:// or file:// syntax") + return false + default: + c.UI.Error(fmt.Sprintf("Error parsing client certificate flag: %v", err)) + return false + } + } + + switch c.flagClientCertificateKey { + case "": + case "null": + *opts = append(*opts, authmethods.DefaultLdapAuthMethodClientCertificateKey()) + default: + key, err := parseutil.MustParsePath(c.flagClientCertificateKey) + switch { + case err == nil: + *opts = append(*opts, authmethods.WithLdapAuthMethodClientCertificateKey(key)) + case errors.Is(err, parseutil.ErrNotParsed): + c.UI.Error("Client certificate key flag must be used with env:// or file:// syntax") + return false + default: + c.UI.Error(fmt.Sprintf("Error parsing client certificate key flag: %v", err)) + return false + } + } + + switch c.flagBindDn { + case "": + case "null": + *opts = append(*opts, authmethods.DefaultLdapAuthMethodBindDn()) + default: + *opts = append(*opts, authmethods.WithLdapAuthMethodBindDn(c.flagBindDn)) + } + + switch c.flagBindPassword { + case "": + case "null": + *opts = append(*opts, authmethods.DefaultLdapAuthMethodBindPassword()) + default: + password, err := parseutil.MustParsePath(c.flagBindPassword) + switch { + case err == nil: + *opts = append(*opts, authmethods.WithLdapAuthMethodBindPassword(password)) + case errors.Is(err, parseutil.ErrNotParsed): + c.UI.Error("Bind password flag must be used with env:// or file:// syntax") + return false + default: + c.UI.Error(fmt.Sprintf("Error parsing bind password flag: %v", err)) + return false + } + } + + switch c.flagUseTokenGroups { + case true: + *opts = append(*opts, authmethods.WithLdapAuthMethodUseTokenGroups(true)) + default: + *opts = append(*opts, authmethods.WithLdapAuthMethodUseTokenGroups(false)) + } + + switch { + case len(c.flagAccountAttributeMaps) == 0: + case len(c.flagAccountAttributeMaps) == 1 && c.flagAccountAttributeMaps[0] == "null": + *opts = append(*opts, authmethods.DefaultLdapAuthMethodAccountAttributeMaps()) + default: + *opts = append(*opts, authmethods.WithLdapAuthMethodAccountAttributeMaps(c.flagAccountAttributeMaps)) + } + + switch c.flagState { + case "": + // there is a default value during "create", so it's okay to not + // specify a state + case "null": + c.UI.Error("State is required, you cannot set it to null") + return false + default: + *opts = append(*opts, authmethods.WithLdapAuthMethodState(c.flagState)) + } + return true +} + +func validateCerts(pems ...string) error { + if len(pems) == 0 { + return errors.New("no PEMs provided") + } + for _, p := range pems { + if p == "" { + return errors.New("empty certificate PEM") + } + block, _ := pem.Decode([]byte(p)) + if block == nil { + return errors.New("failed to parse certificate: invalid PEM encoding") + } + _, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return fmt.Errorf("failed to parse certificate: invalid block: %w", err) + } + } + return nil +} diff --git a/internal/cmd/commands/managedgroupscmd/ldap_funcs.go b/internal/cmd/commands/managedgroupscmd/ldap_funcs.go new file mode 100644 index 0000000000..500fa5557f --- /dev/null +++ b/internal/cmd/commands/managedgroupscmd/ldap_funcs.go @@ -0,0 +1,86 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package managedgroupscmd + +import ( + "fmt" + + "github.com/hashicorp/boundary/api/managedgroups" + "github.com/hashicorp/boundary/internal/cmd/base" +) + +const ( + groupNamesFlagName = "group-names" +) + +type extraLdapCmdVars struct { + flagGroupNames []string +} + +func init() { + extraLdapFlagsFunc = extraLdapFlagsFuncImpl + extraLdapActionsFlagsMapFunc = extraLdapActionsFlagsMapFuncImpl + extraLdapFlagsHandlingFunc = extraLdapFlagsHandlingFuncImpl +} + +func extraLdapActionsFlagsMapFuncImpl() map[string][]string { + return map[string][]string{ + "create": {groupNamesFlagName}, + "update": {groupNamesFlagName}, + } +} + +func (c *LdapCommand) extraLdapHelpFunc(helpMap map[string]func() string) string { + var helpStr string + switch c.Func { + case "create": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary managed-groups create ldap [options] [args]", + "", + " Create a ldap-type managed group. Example:", + "", + ` $ boundary managed-groups create ldap -group-names admin -description "Ldap managed group for ProdOps"`, + "", + "", + }) + + case "update": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary managed-groups update ldap [options] [args]", + "", + " Update an ldap-type managed group given its ID. Example:", + "", + ` $ boundary managed-groups update ldap -id acctldap_1234567890 -name "devops" -description "Ldap managed group for DevOps"`, + "", + "", + }) + } + return helpStr + c.Flags().Help() +} + +func extraLdapFlagsFuncImpl(c *LdapCommand, _ *base.FlagSets, f *base.FlagSet) { + for _, name := range flagsLdapMap[c.Func] { + switch name { + case groupNamesFlagName: + f.StringSliceVar(&base.StringSliceVar{ + Name: groupNamesFlagName, + Target: &c.flagGroupNames, + Usage: "The LDAP group names against which an LDAP account's associated groups (discovered during login) will be evaluated to determine membership (required). May be specified multiple times", + }) + } + } +} + +func extraLdapFlagsHandlingFuncImpl(c *LdapCommand, _ *base.FlagSets, opts *[]managedgroups.Option) bool { + switch { + case len(c.flagGroupNames) == 0: + case len(c.flagGroupNames) == 1 && c.flagGroupNames[0] == "null": + c.UI.Error(fmt.Sprintf("There must be at least one %q", groupNamesFlagName)) + return false + default: + *opts = append(*opts, managedgroups.WithLdapManagedGroupGroupNames(c.flagGroupNames)) + } + + return true +} diff --git a/internal/cmd/commands/managedgroupscmd/ldap_managedgroups.gen.go b/internal/cmd/commands/managedgroupscmd/ldap_managedgroups.gen.go new file mode 100644 index 0000000000..1582889906 --- /dev/null +++ b/internal/cmd/commands/managedgroupscmd/ldap_managedgroups.gen.go @@ -0,0 +1,275 @@ +// Code generated by "make cli"; DO NOT EDIT. +package managedgroupscmd + +import ( + "errors" + "fmt" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/managedgroups" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/boundary/internal/cmd/common" + "github.com/hashicorp/go-secure-stdlib/strutil" + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +func initLdapFlags() { + flagsOnce.Do(func() { + extraFlags := extraLdapActionsFlagsMapFunc() + for k, v := range extraFlags { + flagsLdapMap[k] = append(flagsLdapMap[k], v...) + } + }) +} + +var ( + _ cli.Command = (*LdapCommand)(nil) + _ cli.CommandAutocomplete = (*LdapCommand)(nil) +) + +type LdapCommand struct { + *base.Command + + Func string + + plural string + + extraLdapCmdVars +} + +func (c *LdapCommand) AutocompleteArgs() complete.Predictor { + initLdapFlags() + return complete.PredictAnything +} + +func (c *LdapCommand) AutocompleteFlags() complete.Flags { + initLdapFlags() + return c.Flags().Completions() +} + +func (c *LdapCommand) Synopsis() string { + if extra := extraLdapSynopsisFunc(c); extra != "" { + return extra + } + + synopsisStr := "managed group" + + synopsisStr = fmt.Sprintf("%s %s", "ldap-type", synopsisStr) + + return common.SynopsisFunc(c.Func, synopsisStr) +} + +func (c *LdapCommand) Help() string { + initLdapFlags() + + var helpStr string + helpMap := common.HelpMap("managed group") + + switch c.Func { + + default: + + helpStr = c.extraLdapHelpFunc(helpMap) + + } + + // Keep linter from complaining if we don't actually generate code using it + _ = helpMap + return helpStr +} + +var flagsLdapMap = map[string][]string{ + + "create": {"auth-method-id", "name", "description"}, + + "update": {"id", "name", "description", "version"}, +} + +func (c *LdapCommand) Flags() *base.FlagSets { + if len(flagsLdapMap[c.Func]) == 0 { + return c.FlagSet(base.FlagSetNone) + } + + set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat) + f := set.NewFlagSet("Command Options") + common.PopulateCommonFlags(c.Command, f, "ldap-type managed group", flagsLdapMap, c.Func) + + extraLdapFlagsFunc(c, set, f) + + return set +} + +func (c *LdapCommand) Run(args []string) int { + initLdapFlags() + + switch c.Func { + case "": + return cli.RunResultHelp + + } + + c.plural = "ldap-type managed group" + switch c.Func { + case "list": + c.plural = "ldap-type managed groups" + } + + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + + if strutil.StrListContains(flagsLdapMap[c.Func], "id") && c.FlagId == "" { + c.PrintCliError(errors.New("ID is required but not passed in via -id")) + return base.CommandUserError + } + + var opts []managedgroups.Option + + if strutil.StrListContains(flagsLdapMap[c.Func], "auth-method-id") { + switch c.Func { + + case "create": + if c.FlagAuthMethodId == "" { + c.PrintCliError(errors.New("AuthMethod ID must be passed in via -auth-method-id or BOUNDARY_AUTH_METHOD_ID")) + return base.CommandUserError + } + + } + } + + client, err := c.Client() + if c.WrapperCleanupFunc != nil { + defer func() { + if err := c.WrapperCleanupFunc(); err != nil { + c.PrintCliError(fmt.Errorf("Error cleaning kms wrapper: %w", err)) + } + }() + } + if err != nil { + c.PrintCliError(fmt.Errorf("Error creating API client: %w", err)) + return base.CommandCliError + } + managedgroupsClient := managedgroups.NewClient(client) + + switch c.FlagName { + case "": + case "null": + opts = append(opts, managedgroups.DefaultName()) + default: + opts = append(opts, managedgroups.WithName(c.FlagName)) + } + + switch c.FlagDescription { + case "": + case "null": + opts = append(opts, managedgroups.DefaultDescription()) + default: + opts = append(opts, managedgroups.WithDescription(c.FlagDescription)) + } + + if c.FlagFilter != "" { + opts = append(opts, managedgroups.WithFilter(c.FlagFilter)) + } + + var version uint32 + + switch c.Func { + + case "update": + switch c.FlagVersion { + case 0: + opts = append(opts, managedgroups.WithAutomaticVersioning(true)) + default: + version = uint32(c.FlagVersion) + } + + } + + if ok := extraLdapFlagsHandlingFunc(c, f, &opts); !ok { + return base.CommandUserError + } + + var resp *api.Response + var item *managedgroups.ManagedGroup + + var createResult *managedgroups.ManagedGroupCreateResult + + var updateResult *managedgroups.ManagedGroupUpdateResult + + switch c.Func { + + case "create": + createResult, err = managedgroupsClient.Create(c.Context, c.FlagAuthMethodId, opts...) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + resp = createResult.GetResponse() + item = createResult.GetItem() + + case "update": + updateResult, err = managedgroupsClient.Update(c.Context, c.FlagId, version, opts...) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + resp = updateResult.GetResponse() + item = updateResult.GetItem() + + } + + resp, item, err = executeExtraLdapActions(c, resp, item, err, managedgroupsClient, version, opts) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + + output, err := printCustomLdapActionOutput(c) + if err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + if output { + return base.CommandSuccess + } + + switch c.Func { + + } + + switch base.Format(c.UI) { + case "table": + c.UI.Output(printItemTable(item, resp)) + + case "json": + if ok := c.PrintJsonItem(resp); !ok { + return base.CommandCliError + } + } + + return base.CommandSuccess +} + +func (c *LdapCommand) checkFuncError(err error) int { + if err == nil { + return 0 + } + if apiErr := api.AsServerError(err); apiErr != nil { + c.PrintApiError(apiErr, fmt.Sprintf("Error from controller when performing %s on %s", c.Func, c.plural)) + return base.CommandApiError + } + c.PrintCliError(fmt.Errorf("Error trying to %s %s: %s", c.Func, c.plural, err.Error())) + return base.CommandCliError +} + +var ( + extraLdapActionsFlagsMapFunc = func() map[string][]string { return nil } + extraLdapSynopsisFunc = func(*LdapCommand) string { return "" } + extraLdapFlagsFunc = func(*LdapCommand, *base.FlagSets, *base.FlagSet) {} + extraLdapFlagsHandlingFunc = func(*LdapCommand, *base.FlagSets, *[]managedgroups.Option) bool { return true } + executeExtraLdapActions = func(_ *LdapCommand, inResp *api.Response, inItem *managedgroups.ManagedGroup, inErr error, _ *managedgroups.Client, _ uint32, _ []managedgroups.Option) (*api.Response, *managedgroups.ManagedGroup, error) { + return inResp, inItem, inErr + } + printCustomLdapActionOutput = func(*LdapCommand) (bool, error) { return false, nil } +) diff --git a/internal/cmd/gencli/input.go b/internal/cmd/gencli/input.go index 0dbd655c33..8b34aaa64a 100644 --- a/internal/cmd/gencli/input.go +++ b/internal/cmd/gencli/input.go @@ -130,6 +130,20 @@ var inputStructs = map[string][]*cmdInfo{ HasDescription: true, VersionedActions: []string{"update"}, }, + { + ResourceType: resource.Account.String(), + Pkg: "accounts", + StdActions: []string{"create", "update"}, + SubActionPrefix: "ldap", + HasExtraCommandVars: true, + SkipNormalHelp: true, + HasExtraHelpFunc: true, + HasId: true, + HasName: true, + Container: "AuthMethod", + HasDescription: true, + VersionedActions: []string{"update"}, + }, }, "authmethods": { { @@ -170,6 +184,21 @@ var inputStructs = map[string][]*cmdInfo{ VersionedActions: []string{"update", "change-state"}, NeedsSubtypeInCreate: true, }, + { + ResourceType: resource.AuthMethod.String(), + Pkg: "authmethods", + StdActions: []string{"create", "update"}, + SubActionPrefix: "ldap", + HasExtraCommandVars: true, + SkipNormalHelp: true, + HasExtraHelpFunc: true, + HasId: true, + HasName: true, + HasDescription: true, + Container: "Scope", + VersionedActions: []string{"update"}, + NeedsSubtypeInCreate: true, + }, }, "authtokens": { { @@ -483,6 +512,20 @@ var inputStructs = map[string][]*cmdInfo{ HasDescription: true, VersionedActions: []string{"update"}, }, + { + ResourceType: resource.ManagedGroup.String(), + Pkg: "managedgroups", + StdActions: []string{"create", "update"}, + SubActionPrefix: "ldap", + HasExtraCommandVars: true, + SkipNormalHelp: true, + HasExtraHelpFunc: true, + HasId: true, + HasName: true, + Container: "AuthMethod", + HasDescription: true, + VersionedActions: []string{"update"}, + }, }, "roles": { { diff --git a/internal/daemon/controller/auth/auth.go b/internal/daemon/controller/auth/auth.go index 98b136d711..1a058f76c3 100644 --- a/internal/daemon/controller/auth/auth.go +++ b/internal/daemon/controller/auth/auth.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/boundary/api/recovery" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth" + "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/daemon/controller/common" @@ -104,6 +105,7 @@ type verifier struct { serversRepoFn common.ServersRepoFactory passwordAuthRepoFn common.PasswordAuthRepoFactory oidcAuthRepoFn common.OidcAuthRepoFactory + ldapAuthRepoFn common.LdapAuthRepoFactory kms *kms.Kms requestInfo *authpb.RequestInfo res *perms.Resource @@ -128,6 +130,7 @@ func NewVerifierContextWithAccounts(ctx context.Context, serversRepoFn common.ServersRepoFactory, passwordAuthRepoFn common.PasswordAuthRepoFactory, oidcAuthRepoFn common.OidcAuthRepoFactory, + ldapAuthRepoFn common.LdapAuthRepoFactory, kms *kms.Kms, requestInfo *authpb.RequestInfo, ) context.Context { @@ -137,6 +140,7 @@ func NewVerifierContextWithAccounts(ctx context.Context, serversRepoFn: serversRepoFn, passwordAuthRepoFn: passwordAuthRepoFn, oidcAuthRepoFn: oidcAuthRepoFn, + ldapAuthRepoFn: ldapAuthRepoFn, kms: kms, requestInfo: requestInfo, }) @@ -153,7 +157,7 @@ func NewVerifierContext(ctx context.Context, kms *kms.Kms, requestInfo *authpb.RequestInfo, ) context.Context { - return NewVerifierContextWithAccounts(ctx, iamRepoFn, authTokenRepoFn, serversRepoFn, nil, nil, kms, requestInfo) + return NewVerifierContextWithAccounts(ctx, iamRepoFn, authTokenRepoFn, serversRepoFn, nil, nil, nil, kms, requestInfo) } // Verify takes in a context that has expected parameters as values and runs an @@ -547,7 +551,7 @@ func (v verifier) performAuthCheck(ctx context.Context) ( userData.User.Email = util.Pointer(u.Email) userData.User.FullName = util.Pointer(u.FullName) - if userData.Account.Id != nil && *userData.Account.Id != "" && v.passwordAuthRepoFn != nil && v.oidcAuthRepoFn != nil { + if userData.Account.Id != nil && *userData.Account.Id != "" && v.passwordAuthRepoFn != nil && v.oidcAuthRepoFn != nil && v.ldapAuthRepoFn != nil { const domain = "auth" var acct auth.Account var err error @@ -566,6 +570,13 @@ func (v verifier) performAuthCheck(ctx context.Context) ( return } acct, err = repo.LookupAccount(ctx, *userData.Account.Id) + case ldap.Subtype: + repo, repoErr := v.ldapAuthRepoFn() + if repoErr != nil { + retErr = errors.Wrap(ctx, repoErr, op, errors.WithMsg("failed to get ldap auth repo")) + return + } + acct, err = repo.LookupAccount(ctx, *userData.Account.Id) default: retErr = errors.Wrap(ctx, err, op, errors.WithMsg("unrecognized account id type")) return diff --git a/internal/daemon/controller/common/common.go b/internal/daemon/controller/common/common.go index 0b2c6e20ec..66b62dcb63 100644 --- a/internal/daemon/controller/common/common.go +++ b/internal/daemon/controller/common/common.go @@ -4,6 +4,7 @@ package common import ( + "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" credstatic "github.com/hashicorp/boundary/internal/credential/static" @@ -22,6 +23,7 @@ type ( StaticCredentialRepoFactory = func() (*credstatic.Repository, error) IamRepoFactory = iam.IamRepoFactory OidcAuthRepoFactory = oidc.OidcRepoFactory + LdapAuthRepoFactory = ldap.RepoFactory PasswordAuthRepoFactory func() (*password.Repository, error) ServersRepoFactory func() (*server.Repository, error) StaticRepoFactory func() (*static.Repository, error) diff --git a/internal/daemon/controller/controller.go b/internal/daemon/controller/controller.go index 6a66095958..39ee14a16c 100644 --- a/internal/daemon/controller/controller.go +++ b/internal/daemon/controller/controller.go @@ -11,6 +11,7 @@ import ( "sync" "sync/atomic" + "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/authtoken" @@ -117,6 +118,7 @@ type Controller struct { StaticCredentialRepoFn common.StaticCredentialRepoFactory IamRepoFn common.IamRepoFactory OidcRepoFn common.OidcAuthRepoFactory + LdapRepoFn common.LdapAuthRepoFactory PasswordAuthRepoFn common.PasswordAuthRepoFactory ServersRepoFn common.ServersRepoFactory SessionRepoFn session.RepositoryFactory @@ -362,6 +364,9 @@ func New(ctx context.Context, conf *Config) (*Controller, error) { c.OidcRepoFn = func() (*oidc.Repository, error) { return oidc.NewRepository(ctx, dbase, dbase, c.kms) } + c.LdapRepoFn = func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, dbase, dbase, c.kms) + } c.PasswordAuthRepoFn = func() (*password.Repository, error) { return password.NewRepository(dbase, dbase, c.kms) } diff --git a/internal/daemon/controller/gateway.go b/internal/daemon/controller/gateway.go index 97b1493948..8d9d95c7b3 100644 --- a/internal/daemon/controller/gateway.go +++ b/internal/daemon/controller/gateway.go @@ -72,6 +72,7 @@ func newGrpcServer( serversRepoFn common.ServersRepoFactory, passwordAuthRepoFn common.PasswordAuthRepoFactory, oidcAuthRepoFn common.OidcAuthRepoFactory, + ldapAuthRepoFn common.LdapAuthRepoFactory, kms *kms.Kms, eventer *event.Eventer, ) (*grpc.Server, string, error) { @@ -80,7 +81,7 @@ func newGrpcServer( if err != nil { return nil, "", errors.Wrap(ctx, err, op, errors.WithMsg("unable to generate gateway ticket")) } - requestCtxInterceptor, err := requestCtxInterceptor(ctx, iamRepoFn, authTokenRepoFn, serversRepoFn, passwordAuthRepoFn, oidcAuthRepoFn, kms, ticket, eventer) + requestCtxInterceptor, err := requestCtxInterceptor(ctx, iamRepoFn, authTokenRepoFn, serversRepoFn, passwordAuthRepoFn, oidcAuthRepoFn, ldapAuthRepoFn, kms, ticket, eventer) if err != nil { return nil, "", err } diff --git a/internal/daemon/controller/handler.go b/internal/daemon/controller/handler.go index dbeb059d29..978045d7a8 100644 --- a/internal/daemon/controller/handler.go +++ b/internal/daemon/controller/handler.go @@ -154,14 +154,14 @@ func (c *Controller) registerGrpcServices(s *grpc.Server) error { services.RegisterHostServiceServer(s, hs) } if _, ok := currentServices[services.AccountService_ServiceDesc.ServiceName]; !ok { - accts, err := accounts.NewService(c.PasswordAuthRepoFn, c.OidcRepoFn) + accts, err := accounts.NewService(c.baseContext, c.PasswordAuthRepoFn, c.OidcRepoFn, c.LdapRepoFn) if err != nil { return fmt.Errorf("failed to create account handler service: %w", err) } services.RegisterAccountServiceServer(s, accts) } if _, ok := currentServices[services.AuthMethodService_ServiceDesc.ServiceName]; !ok { - authMethods, err := authmethods.NewService(c.kms, c.PasswordAuthRepoFn, c.OidcRepoFn, c.IamRepoFn, c.AuthTokenRepoFn) + authMethods, err := authmethods.NewService(c.kms, c.PasswordAuthRepoFn, c.OidcRepoFn, c.IamRepoFn, c.AuthTokenRepoFn, c.LdapRepoFn) if err != nil { return fmt.Errorf("failed to create auth method handler service: %w", err) } @@ -229,7 +229,7 @@ func (c *Controller) registerGrpcServices(s *grpc.Server) error { services.RegisterSessionServiceServer(s, ss) } if _, ok := currentServices[services.ManagedGroupService_ServiceDesc.ServiceName]; !ok { - mgs, err := managed_groups.NewService(c.OidcRepoFn) + mgs, err := managed_groups.NewService(c.baseContext, c.OidcRepoFn, c.LdapRepoFn) if err != nil { return fmt.Errorf("failed to create managed groups handler service: %w", err) } diff --git a/internal/daemon/controller/handlers/accounts/account_service.go b/internal/daemon/controller/handlers/accounts/account_service.go index 01ef20aede..81667ca8ae 100644 --- a/internal/daemon/controller/handlers/accounts/account_service.go +++ b/internal/daemon/controller/handlers/accounts/account_service.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth" + "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" oidcstore "github.com/hashicorp/boundary/internal/auth/oidc/store" "github.com/hashicorp/boundary/internal/auth/password" @@ -52,6 +53,13 @@ const ( nameClaimField = "attributes.full_name" emailClaimField = "attributes.email" + // ldap field names + loginAttrField = "attributes.login_name" + nameAttrField = "attributes.full_name" + emailAttrField = "attributes.email" + dnAttrField = "attributes.dn" + memberOfAttrField = "attributes.member_of_groups" + domain = "auth" ) @@ -76,6 +84,12 @@ var ( action.Update, action.Delete, }, + ldap.Subtype: { + action.NoOp, + action.Read, + action.Update, + action.Delete, + }, } // CollectionActions contains the set of actions that can be performed on @@ -102,20 +116,23 @@ type Service struct { pwRepoFn common.PasswordAuthRepoFactory oidcRepoFn common.OidcAuthRepoFactory + ldapRepoFn common.LdapAuthRepoFactory } var _ pbs.AccountServiceServer = (*Service)(nil) // NewService returns a account service which handles account related requests to boundary. -func NewService(pwRepo common.PasswordAuthRepoFactory, oidcRepo common.OidcAuthRepoFactory) (Service, error) { +func NewService(ctx context.Context, pwRepo common.PasswordAuthRepoFactory, oidcRepo common.OidcAuthRepoFactory, ldapRepo common.LdapAuthRepoFactory) (Service, error) { const op = "accounts.NewService" - if pwRepo == nil { - return Service{}, errors.NewDeprecated(errors.InvalidParameter, op, "missing password repository") - } - if oidcRepo == nil { - return Service{}, errors.NewDeprecated(errors.InvalidParameter, op, "missing oidc repository provided") - } - return Service{pwRepoFn: pwRepo, oidcRepoFn: oidcRepo}, nil + switch { + case pwRepo == nil: + return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing password repository") + case oidcRepo == nil: + return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing oidc repository") + case ldapRepo == nil: + return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing ldap repository") + } + return Service{pwRepoFn: pwRepo, oidcRepoFn: oidcRepo, ldapRepoFn: ldapRepo}, nil } // ListAccounts implements the interface pbs.AccountServiceServer. @@ -434,6 +451,29 @@ func (s Service) getFromRepo(ctx context.Context, id string) (auth.Account, []st mgIds = append(mgIds, mg.GetManagedGroupId()) } acct = a + case ldap.Subtype: + repo, err := s.ldapRepoFn() + if err != nil { + return nil, nil, err + } + a, err := repo.LookupAccount(ctx, id) + if err != nil { + return nil, nil, err + } + if err != nil { + if errors.IsNotFoundError(err) { + return nil, nil, handlers.NotFoundErrorf("LDAP account %q doesn't exist.", id) + } + return nil, nil, err + } + mgs, err := repo.ListManagedGroupMembershipsByMember(ctx, a.GetPublicId()) + if err != nil { + return nil, nil, err + } + for _, mg := range mgs { + mgIds = append(mgIds, mg.GetManagedGroupId()) + } + acct = a default: return nil, nil, handlers.NotFoundErrorf("Unrecognized id.") } @@ -515,6 +555,37 @@ func (s Service) createOidcInRepo(ctx context.Context, am auth.AuthMethod, item return out, nil } +func (s Service) createLdapInRepo(ctx context.Context, am auth.AuthMethod, item *pb.Account) (*ldap.Account, error) { + const op = "accounts.(Service).createLdapInRepo" + if item == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing item") + } + var opts []ldap.Option + if item.GetName() != nil { + opts = append(opts, ldap.WithName(ctx, item.GetName().GetValue())) + } + if item.GetDescription() != nil { + opts = append(opts, ldap.WithDescription(ctx, item.GetDescription().GetValue())) + } + a, err := ldap.NewAccount(ctx, am.GetScopeId(), am.GetPublicId(), item.GetLdapAccountAttributes().GetLoginName(), opts...) + if err != nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build account for creation: %v.", err) + } + repo, err := s.ldapRepoFn() + if err != nil { + return nil, err + } + + out, err := repo.CreateAccount(ctx, a) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create account")) + } + if out == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create account but no error returned from repository.") + } + return out, nil +} + func (s Service) createInRepo(ctx context.Context, am auth.AuthMethod, item *pb.Account) (auth.Account, error) { const op = "accounts.(Service).createInRepo" if item == nil { @@ -540,7 +611,17 @@ func (s Service) createInRepo(ctx context.Context, am auth.AuthMethod, item *pb. return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create account but no error returned from repository.") } out = am + case ldap.Subtype: + am, err := s.createLdapInRepo(ctx, am, item) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + if am == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create ldap account but no error returned from repository.") + } + out = am } + return out, nil } @@ -611,6 +692,63 @@ func (s Service) updateOidcInRepo(ctx context.Context, scopeId, amId, id string, return out, nil } +func (s Service) updateLdapInRepo(ctx context.Context, scopeId, amId, id string, mask []string, item *pb.Account) (*ldap.Account, error) { + const op = "accounts.(Service).updateLdapInRepo" + switch { + case item == nil: + return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "nil account.") + case scopeId == "": + return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "missing scope id") + case amId == "": + return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "missing auth method id") + case id == "": + return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "missing id") + case len(mask) == 0: + return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "missing mask.") + } + u := ldap.AllocAccount() + u.PublicId = id + u.ScopeId = scopeId + u.AuthMethodId = amId + if item.GetName() != nil { + u.Name = item.GetName().GetValue() + } + if item.GetDescription() != nil { + u.Description = item.GetDescription().GetValue() + } + + // we don't need a mask mgr, since none of the attributes fields are + // updatable. Just a simple split on commas looking for multiple paths in + // one mask string + dbMask := []string{} + for _, v := range mask { + vSplit := strings.Split(v, ",") + for _, m := range vSplit { + switch m { + case globals.NameField, globals.DescriptionField: + dbMask = append(dbMask, vSplit...) + case globals.VersionField: + // no-op + default: + return nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."}) + } + } + } + + repo, err := s.ldapRepoFn() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + out, rowsUpdated, err := repo.UpdateAccount(ctx, scopeId, u, item.GetVersion(), dbMask) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update account")) + } + if rowsUpdated == 0 { + return nil, handlers.NotFoundErrorf("Account %q doesn't exist or incorrect version provided.", id) + } + return out, nil +} + func (s Service) updateInRepo(ctx context.Context, scopeId, authMethodId string, req *pbs.UpdateAccountRequest) (auth.Account, error) { const op = "accounts.(Service).updateInRepo" var out auth.Account @@ -633,6 +771,15 @@ func (s Service) updateInRepo(ctx context.Context, scopeId, authMethodId string, return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to update account but no error returned from repository.") } out = a + case ldap.Subtype: + a, err := s.updateLdapInRepo(ctx, scopeId, authMethodId, req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem()) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + if a == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to update account but no error returned from repository.") + } + out = a } return out, nil } @@ -654,6 +801,12 @@ func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool, return false, iErr } rows, err = repo.DeleteAccount(ctx, scopeId, id) + case ldap.Subtype: + repo, iErr := s.ldapRepoFn() + if iErr != nil { + return false, iErr + } + rows, err = repo.DeleteAccount(ctx, id) } if err != nil { if errors.IsNotFoundError(err) { @@ -693,6 +846,18 @@ func (s Service) listFromRepo(ctx context.Context, authMethodId string) ([]auth. for _, a := range oidcl { outUl = append(outUl, a) } + case ldap.Subtype: + ldapRepo, err := s.ldapRepoFn() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + ldapList, err := ldapRepo.ListAccounts(ctx, authMethodId) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + for _, a := range ldapList { + outUl = append(outUl, a) + } } return outUl, nil } @@ -756,6 +921,11 @@ func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Ty res.Error = err return nil, res } + ldapRepo, err := s.ldapRepoFn() + if err != nil { + res.Error = err + return nil, res + } var parentId string opts := []requestauth.Option{requestauth.WithType(resource.Account), requestauth.WithAction(a)} @@ -786,6 +956,17 @@ func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Ty return nil, res } parentId = acct.GetAuthMethodId() + case ldap.Subtype: + acct, err := ldapRepo.LookupAccount(ctx, id) + if err != nil { + res.Error = err + return nil, res + } + if acct == nil { + res.Error = handlers.NotFoundError() + return nil, res + } + parentId = acct.GetAuthMethodId() } opts = append(opts, requestauth.WithId(id)) } @@ -814,6 +995,17 @@ func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Ty return nil, res } authMeth = am + case ldap.Subtype: + am, err := ldapRepo.LookupAuthMethod(ctx, parentId) + if err != nil { + res.Error = err + return nil, res + } + if am == nil { + res.Error = handlers.NotFoundError() + return nil, res + } + authMeth = am } opts = append(opts, requestauth.WithScopeId(authMeth.GetScopeId()), requestauth.WithPin(parentId)) return authMeth, requestauth.Verify(ctx, opts...) @@ -908,6 +1100,29 @@ func toProto(ctx context.Context, in auth.Account, opt ...handlers.Option) (*pb. } } out.Attrs = attrs + case *ldap.Account: + if outputFields.Has(globals.TypeField) { + out.Type = ldap.Subtype.String() + } + if !outputFields.Has(globals.AttributesField) { + break + } + attrs := &pb.Account_LdapAccountAttributes{ + LdapAccountAttributes: &pb.LdapAccountAttributes{ + LoginName: i.GetLoginName(), + FullName: i.GetFullName(), + Email: i.GetEmail(), + Dn: i.GetDn(), + }, + } + if encodedGroups := i.GetMemberOfGroups(); encodedGroups != "" { + var decodedGroups []string + if err := json.Unmarshal([]byte(encodedGroups), &decodedGroups); err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("")) + } + attrs.LdapAccountAttributes.MemberOfGroups = decodedGroups + } + out.Attrs = attrs } return &out, nil } @@ -946,7 +1161,7 @@ func validateGetRequest(req *pbs.GetAccountRequest) error { if req == nil { return errors.NewDeprecated(errors.InvalidParameter, op, "nil request") } - return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, globals.PasswordAccountPreviousPrefix, globals.PasswordAccountPrefix, globals.OidcAccountPrefix) + return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, globals.PasswordAccountPreviousPrefix, globals.PasswordAccountPrefix, globals.OidcAccountPrefix, globals.LdapAccountPrefix) } func validateCreateRequest(req *pbs.CreateAccountRequest) error { @@ -1001,6 +1216,31 @@ func validateCreateRequest(req *pbs.CreateAccountRequest) error { badFields[emailClaimField] = "This is a read only field." } } + case ldap.Subtype: + if req.GetItem().GetType() != "" && req.GetItem().GetType() != ldap.Subtype.String() { + badFields[typeField] = "Doesn't match the parent resource's type." + } + attrs := req.GetItem().GetLdapAccountAttributes() + switch { + case attrs == nil: + badFields["attributes"] = "This is a required field." + default: + if attrs.GetLoginName() == "" { + badFields[loginAttrField] = "This is a required field for this type." + } + if attrs.GetFullName() != "" { + badFields[nameAttrField] = "This is a read only field." + } + if attrs.GetEmail() != "" { + badFields[emailAttrField] = "This is a read only field." + } + if attrs.GetDn() != "" { + badFields[dnAttrField] = "This is a read only field." + } + if len(attrs.GetMemberOfGroups()) > 0 { + badFields[memberOfAttrField] = "This is a read only field." + } + } default: badFields[authMethodIdField] = "Unknown auth method type from ID." } @@ -1036,9 +1276,28 @@ func validateUpdateRequest(req *pbs.UpdateAccountRequest) error { if handlers.MaskContains(req.GetUpdateMask().GetPaths(), nameClaimField) { badFields[nameClaimField] = "Field is read only." } + case ldap.Subtype: + if req.GetItem().GetType() != "" && req.GetItem().GetType() != ldap.Subtype.String() { + badFields[typeField] = "Cannot modify the resource type." + } + if handlers.MaskContains(req.GetUpdateMask().GetPaths(), loginAttrField) { + badFields[loginAttrField] = "Field cannot be updated." + } + if handlers.MaskContains(req.GetUpdateMask().GetPaths(), nameAttrField) { + badFields[nameAttrField] = "Field cannot be updated." + } + if handlers.MaskContains(req.GetUpdateMask().GetPaths(), emailAttrField) { + badFields[emailAttrField] = "Field cannot be updated." + } + if handlers.MaskContains(req.GetUpdateMask().GetPaths(), dnAttrField) { + badFields[dnAttrField] = "Field cannot be updated." + } + if handlers.MaskContains(req.GetUpdateMask().GetPaths(), memberOfAttrField) { + badFields[memberOfAttrField] = "Field cannot be updated." + } } return badFields - }, globals.PasswordAccountPreviousPrefix, globals.PasswordAccountPrefix, globals.OidcAccountPrefix) + }, globals.PasswordAccountPreviousPrefix, globals.PasswordAccountPrefix, globals.OidcAccountPrefix, globals.LdapAccountPrefix) } func validateDeleteRequest(req *pbs.DeleteAccountRequest) error { @@ -1046,7 +1305,7 @@ func validateDeleteRequest(req *pbs.DeleteAccountRequest) error { if req == nil { return errors.NewDeprecated(errors.InvalidParameter, op, "nil request") } - return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, globals.PasswordAccountPreviousPrefix, globals.PasswordAccountPrefix, globals.OidcAccountPrefix) + return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, globals.PasswordAccountPreviousPrefix, globals.PasswordAccountPrefix, globals.OidcAccountPrefix, globals.LdapAccountPrefix) } func validateListRequest(req *pbs.ListAccountsRequest) error { @@ -1055,7 +1314,7 @@ func validateListRequest(req *pbs.ListAccountsRequest) error { return errors.NewDeprecated(errors.InvalidParameter, op, "nil request") } badFields := map[string]string{} - if !handlers.ValidId(handlers.Id(req.GetAuthMethodId()), globals.PasswordAuthMethodPrefix, globals.OidcAuthMethodPrefix) { + if !handlers.ValidId(handlers.Id(req.GetAuthMethodId()), globals.PasswordAuthMethodPrefix, globals.OidcAuthMethodPrefix, globals.LdapAuthMethodPrefix) { badFields[authMethodIdField] = "Invalid formatted identifier." } if _, err := handlers.NewFilter(req.GetFilter()); err != nil { diff --git a/internal/daemon/controller/handlers/accounts/account_service_test.go b/internal/daemon/controller/handlers/accounts/account_service_test.go index 7ac5ab20f5..43efa922e6 100644 --- a/internal/daemon/controller/handlers/accounts/account_service_test.go +++ b/internal/daemon/controller/handlers/accounts/account_service_test.go @@ -13,6 +13,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" requestauth "github.com/hashicorp/boundary/internal/daemon/controller/auth" @@ -54,6 +55,12 @@ var ( action.Update.String(), action.Delete.String(), } + ldapAuthorizedActions = []string{ + action.NoOp.String(), + action.Read.String(), + action.Update.String(), + action.Delete.String(), + } ) func TestNewService(t *testing.T) { @@ -68,6 +75,9 @@ func TestNewService(t *testing.T) { oidcRepoFn := func() (*oidc.Repository, error) { return oidc.NewRepository(ctx, rw, rw, kmsCache) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kmsCache) + } cases := []struct { name string @@ -97,7 +107,7 @@ func TestNewService(t *testing.T) { } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - _, err := accounts.NewService(tc.pwRepo, tc.oidcRepo) + _, err := accounts.NewService(ctx, tc.pwRepo, tc.oidcRepo, ldapRepoFn) if tc.wantErr { assert.Error(t, err) } else { @@ -122,8 +132,11 @@ func TestGet(t *testing.T) { iamRepoFn := func() (*iam.Repository, error) { return iam.NewRepository(rw, rw, kmsCache) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kmsCache) + } - s, err := accounts.NewService(pwRepoFn, oidcRepoFn) + s, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn) require.NoError(t, err, "Couldn't create new auth token service.") org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) @@ -176,12 +189,46 @@ func TestGet(t *testing.T) { ManagedGroupIds: []string{mg.GetPublicId()}, } + ldapAm := ldap.TestAuthMethod(t, conn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}) + ldapAcct := ldap.TestAccount(t, conn, ldapAm, "test-acct", + ldap.WithMemberOfGroups(ctx, "admin"), + ldap.WithFullName(ctx, "test-name"), + ldap.WithEmail(ctx, "test-email"), + ldap.WithDn(ctx, "test-dn"), + ) + ldapMg := ldap.TestManagedGroup(t, conn, ldapAm, []string{"admin"}) + ldapWireAccount := pb.Account{ + Id: ldapAcct.GetPublicId(), + AuthMethodId: ldapAm.GetPublicId(), + CreatedTime: ldapAcct.GetCreateTime().GetTimestamp(), + UpdatedTime: ldapAcct.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: org.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, + Version: 1, + Attrs: &pb.Account_LdapAccountAttributes{ + LdapAccountAttributes: &pb.LdapAccountAttributes{ + LoginName: ldapAcct.GetLoginName(), + FullName: ldapAcct.GetFullName(), + Email: ldapAcct.GetEmail(), + Dn: ldapAcct.GetDn(), + MemberOfGroups: []string{"admin"}, + }, + }, + Type: ldap.Subtype.String(), + AuthorizedActions: ldapAuthorizedActions, + ManagedGroupIds: []string{ldapMg.GetPublicId()}, + } + cases := []struct { name string req *pbs.GetAccountRequest res *pbs.GetAccountResponse err error }{ + { + name: "Get an ldap account", + req: &pbs.GetAccountRequest{Id: ldapWireAccount.GetId()}, + res: &pbs.GetAccountResponse{Item: &ldapWireAccount}, + }, { name: "Get a password account", req: &pbs.GetAccountRequest{Id: pwWireAccount.GetId()}, @@ -210,6 +257,12 @@ func TestGet(t *testing.T) { res: nil, err: handlers.ApiErrorWithCode(codes.NotFound), }, + { + name: "Get a non existing ldap account", + req: &pbs.GetAccountRequest{Id: globals.LdapAccountPrefix + "_DoesntExis"}, + res: nil, + err: handlers.ApiErrorWithCode(codes.NotFound), + }, { name: "Wrong id prefix", req: &pbs.GetAccountRequest{Id: "j_1234567890"}, @@ -265,6 +318,9 @@ func TestListPassword(t *testing.T) { iamRepoFn := func() (*iam.Repository, error) { return iam.NewRepository(rw, rw, kms) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kms) + } o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) ams := password.TestAuthMethods(t, conn, o.GetPublicId(), 3) @@ -357,7 +413,7 @@ func TestListPassword(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := accounts.NewService(pwRepoFn, oidcRepoFn) + s, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn) require.NoError(err, "Couldn't create new user service.") // Test non-anon first @@ -403,6 +459,9 @@ func TestListOidc(t *testing.T) { iamRepoFn := func() (*iam.Repository, error) { return iam.NewRepository(rw, rw, kmsCache) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kmsCache) + } o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) databaseWrapper, err := kmsCache.GetWrapper(ctx, o.PublicId, kms.KeyPurposeDatabase) @@ -511,7 +570,7 @@ func TestListOidc(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := accounts.NewService(pwRepoFn, oidcRepoFn) + s, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn) require.NoError(err, "Couldn't create new user service.") got, gErr := s.ListAccounts(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req) @@ -545,6 +604,161 @@ func TestListOidc(t *testing.T) { } } +func TestListLdap(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrap := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrap) + pwRepoFn := func() (*password.Repository, error) { + return password.NewRepository(rw, rw, kmsCache) + } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(ctx, rw, rw, kmsCache) + } + iamRepoFn := func() (*iam.Repository, error) { + return iam.NewRepository(rw, rw, kmsCache) + } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kmsCache) + } + + o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) + databaseWrapper, err := kmsCache.GetWrapper(ctx, o.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + amNoAccounts := ldap.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, []string{"ldaps://no-accounts"}) + amSomeAccounts := ldap.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, []string{"ldaps://some-accounts"}) + amOtherAccounts := ldap.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, []string{"ldaps://other-accounts"}) + + var wantSomeAccounts []*pb.Account + for i := 0; i < 3; i++ { + loginName := fmt.Sprintf("test-login-name%d", i) + aa := ldap.TestAccount(t, conn, amSomeAccounts, loginName) + wantSomeAccounts = append(wantSomeAccounts, &pb.Account{ + Id: aa.GetPublicId(), + AuthMethodId: aa.GetAuthMethodId(), + CreatedTime: aa.GetCreateTime().GetTimestamp(), + UpdatedTime: aa.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, + Version: 1, + Type: ldap.Subtype.String(), + Attrs: &pb.Account_LdapAccountAttributes{ + LdapAccountAttributes: &pb.LdapAccountAttributes{ + LoginName: loginName, + }, + }, + AuthorizedActions: ldapAuthorizedActions, + }) + } + + var wantOtherAccounts []*pb.Account + for i := 0; i < 3; i++ { + loginName := fmt.Sprintf("test-login-name%d", i) + aa := ldap.TestAccount(t, conn, amOtherAccounts, loginName) + wantOtherAccounts = append(wantOtherAccounts, &pb.Account{ + Id: aa.GetPublicId(), + AuthMethodId: aa.GetAuthMethodId(), + CreatedTime: aa.GetCreateTime().GetTimestamp(), + UpdatedTime: aa.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, + Version: 1, + Type: ldap.Subtype.String(), + Attrs: &pb.Account_LdapAccountAttributes{ + LdapAccountAttributes: &pb.LdapAccountAttributes{ + LoginName: loginName, + }, + }, + AuthorizedActions: ldapAuthorizedActions, + }) + } + + cases := []struct { + name string + req *pbs.ListAccountsRequest + res *pbs.ListAccountsResponse + err error + skipAnon bool + }{ + { + name: "List Some Accounts", + req: &pbs.ListAccountsRequest{AuthMethodId: amSomeAccounts.GetPublicId()}, + res: &pbs.ListAccountsResponse{Items: wantSomeAccounts}, + }, + { + name: "List Other Accounts", + req: &pbs.ListAccountsRequest{AuthMethodId: amOtherAccounts.GetPublicId()}, + res: &pbs.ListAccountsResponse{Items: wantOtherAccounts}, + }, + { + name: "List No Accounts", + req: &pbs.ListAccountsRequest{AuthMethodId: amNoAccounts.GetPublicId()}, + res: &pbs.ListAccountsResponse{}, + }, + { + name: "Unfound Auth Method", + req: &pbs.ListAccountsRequest{AuthMethodId: globals.OidcAuthMethodPrefix + "_DoesntExis"}, + err: handlers.ApiErrorWithCode(codes.NotFound), + }, + { + name: "Filter Some Accounts", + req: &pbs.ListAccountsRequest{ + AuthMethodId: amSomeAccounts.GetPublicId(), + Filter: fmt.Sprintf(`"/item/attributes/login_name"==%q`, wantSomeAccounts[1].GetLdapAccountAttributes().LoginName), + }, + res: &pbs.ListAccountsResponse{Items: wantSomeAccounts[1:2]}, + skipAnon: true, + }, + { + name: "Filter All Accounts", + req: &pbs.ListAccountsRequest{ + AuthMethodId: amSomeAccounts.GetPublicId(), + Filter: `"/item/id"=="noaccountmatchesthis"`, + }, + res: &pbs.ListAccountsResponse{}, + }, + { + name: "Filter Bad Format", + req: &pbs.ListAccountsRequest{AuthMethodId: amSomeAccounts.GetPublicId(), Filter: `"//id/"=="bad"`}, + err: handlers.InvalidArgumentErrorf("bad format", nil), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + s, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn) + require.NoError(err, "Couldn't create new user service.") + + got, gErr := s.ListAccounts(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req) + if tc.err != nil { + require.Error(gErr) + assert.True(errors.Is(gErr, tc.err), "ListAccounts() with auth method %q got error %v, wanted %v", tc.req, gErr, tc.err) + return + } else { + require.NoError(gErr) + } + sort.Slice(got.Items, func(i, j int) bool { + return strings.Compare(got.Items[i].GetLdapAccountAttributes().LoginName, + got.Items[j].GetLdapAccountAttributes().LoginName) < 0 + }) + assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListAccounts() with scope %q got response %q, wanted %q", tc.req, got, tc.res) + + // Now test with anon + if tc.skipAnon { + return + } + got, gErr = s.ListAccounts(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId(), requestauth.WithUserId(globals.AnonymousUserId)), tc.req) + require.NoError(gErr) + assert.Len(got.Items, len(tc.res.Items)) + for _, g := range got.GetItems() { + assert.Nil(g.Attrs) + assert.Nil(g.CreatedTime) + assert.Nil(g.UpdatedTime) + assert.Empty(g.Version) + } + }) + } +} + func TestDelete(t *testing.T) { ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") @@ -560,6 +774,9 @@ func TestDelete(t *testing.T) { iamRepoFn := func() (*iam.Repository, error) { return iam.NewRepository(rw, rw, kmsCache) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kmsCache) + } o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) am1 := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0] @@ -576,15 +793,19 @@ func TestDelete(t *testing.T) { ) oidcA := oidc.TestAccount(t, conn, oidcAm, "test-subject") - s, err := accounts.NewService(pwRepoFn, oidcRepoFn) + ldapAm := ldap.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, []string{"ldaps://ldap1"}) + ldapAcct := ldap.TestAccount(t, conn, ldapAm, "test-account") + + s, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn) require.NoError(t, err, "Error when getting new user service.") cases := []struct { - name string - scope string - req *pbs.DeleteAccountRequest - res *pbs.DeleteAccountResponse - err error + name string + scope string + req *pbs.DeleteAccountRequest + res *pbs.DeleteAccountResponse + err error + errContains string }{ { name: "Delete an existing pw account", @@ -598,33 +819,51 @@ func TestDelete(t *testing.T) { Id: oidcA.GetPublicId(), }, }, + { + name: "Delete an existing ldap account", + req: &pbs.DeleteAccountRequest{ + Id: ldapAcct.GetPublicId(), + }, + }, { name: "Delete bad old pw account id", req: &pbs.DeleteAccountRequest{ Id: globals.PasswordAccountPreviousPrefix + "_doesntexis", }, - err: handlers.ApiErrorWithCode(codes.NotFound), + err: handlers.ApiErrorWithCode(codes.NotFound), + errContains: "Resource not found.", }, { name: "Delete bad new pw account id", req: &pbs.DeleteAccountRequest{ Id: globals.PasswordAccountPrefix + "_doesntexis", }, - err: handlers.ApiErrorWithCode(codes.NotFound), + err: handlers.ApiErrorWithCode(codes.NotFound), + errContains: "Resource not found.", }, { name: "Delete bad oidc account id", req: &pbs.DeleteAccountRequest{ Id: globals.OidcAccountPrefix + "_doesntexis", }, - err: handlers.ApiErrorWithCode(codes.NotFound), + err: handlers.ApiErrorWithCode(codes.NotFound), + errContains: "Resource not found.", + }, + { + name: "Delete bad ldap account id", + req: &pbs.DeleteAccountRequest{ + Id: globals.LdapAccountPrefix + "_doesntexis", + }, + err: handlers.ApiErrorWithCode(codes.NotFound), + errContains: "Resource not found.", }, { name: "Bad account id formatting", req: &pbs.DeleteAccountRequest{ Id: "bad_format", }, - err: handlers.ApiErrorWithCode(codes.InvalidArgument), + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "Incorrectly formatted identifier.", }, } for _, tc := range cases { @@ -634,6 +873,8 @@ func TestDelete(t *testing.T) { if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "DeleteAccount(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) + require.NotEmpty(tc.errContains) + assert.Contains(gErr.Error(), tc.errContains) } assert.EqualValuesf(tc.res, got, "DeleteAccount(%q) got response %q, wanted %q", tc.req, got, tc.res) }) @@ -656,12 +897,15 @@ func TestDelete_twice(t *testing.T) { iamRepoFn := func() (*iam.Repository, error) { return iam.NewRepository(rw, rw, kms) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kms) + } o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) am := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0] ac := password.TestAccount(t, conn, am.GetPublicId(), "name1") - s, err := accounts.NewService(pwRepoFn, oidcRepoFn) + s, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn) require.NoError(err, "Error when getting new user service") req := &pbs.DeleteAccountRequest{ Id: ac.GetPublicId(), @@ -688,8 +932,11 @@ func TestCreatePassword(t *testing.T) { iamRepoFn := func() (*iam.Repository, error) { return iam.NewRepository(rw, rw, kms) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kms) + } - s, err := accounts.NewService(pwRepoFn, oidcRepoFn) + s, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn) require.NoError(t, err, "Error when getting new account service.") o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) @@ -932,8 +1179,11 @@ func TestCreateOidc(t *testing.T) { iamRepoFn := func() (*iam.Repository, error) { return iam.NewRepository(rw, rw, kmsCache) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kmsCache) + } - s, err := accounts.NewService(pwRepoFn, oidcRepoFn) + s, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn) require.NoError(t, err, "Error when getting new account service.") o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) @@ -1151,90 +1401,762 @@ func TestCreateOidc(t *testing.T) { } } -func TestUpdatePassword(t *testing.T) { - ctx := context.TODO() +func TestCreateLdap(t *testing.T) { + ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) wrap := db.TestWrapper(t) - kms := kms.TestKms(t, conn, wrap) + kmsCache := kms.TestKms(t, conn, wrap) pwRepoFn := func() (*password.Repository, error) { - return password.NewRepository(rw, rw, kms) + return password.NewRepository(rw, rw, kmsCache) } oidcRepoFn := func() (*oidc.Repository, error) { - return oidc.NewRepository(ctx, rw, rw, kms) + return oidc.NewRepository(ctx, rw, rw, kmsCache) } iamRepoFn := func() (*iam.Repository, error) { - return iam.NewRepository(rw, rw, kms) - } - - o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) - am := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0] - tested, err := accounts.NewService(pwRepoFn, oidcRepoFn) - require.NoError(t, err, "Error when getting new accounts service.") - - defaultScopeInfo := &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()} - defaultAttributes := &pb.Account_PasswordAccountAttributes{ - &pb.PasswordAccountAttributes{ - LoginName: "default", - }, + return iam.NewRepository(rw, rw, kmsCache) } - modifiedAttributes := &pb.Account_PasswordAccountAttributes{ - &pb.PasswordAccountAttributes{ - LoginName: "modified", - }, + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kmsCache) } - freshAccount := func(t *testing.T) (*pb.Account, func()) { - t.Helper() - acc, err := tested.CreateAccount(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), - &pbs.CreateAccountRequest{ - Item: &pb.Account{ - AuthMethodId: am.GetPublicId(), - Name: wrapperspb.String("default"), - Description: wrapperspb.String("default"), - Type: "password", - Attrs: defaultAttributes, - }, - }, - ) - require.NoError(t, err) - - clean := func() { - _, err := tested.DeleteAccount(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), - &pbs.DeleteAccountRequest{Id: acc.GetItem().GetId()}) - require.NoError(t, err) - } + s, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn) + require.NoError(t, err, "Error when getting new account service.") - return acc.GetItem(), clean - } + o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) + databaseWrapper, err := kmsCache.GetWrapper(ctx, o.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + am := ldap.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, []string{"ldaps://ldap1"}) cases := []struct { - name string - req *pbs.UpdateAccountRequest - res *pbs.UpdateAccountResponse - err error + name string + req *pbs.CreateAccountRequest + res *pbs.CreateAccountResponse + err error + errContains string }{ { - name: "Update an Existing AuthMethod", - req: &pbs.UpdateAccountRequest{ - UpdateMask: &field_mask.FieldMask{ - Paths: []string{globals.NameField, globals.DescriptionField}, + name: "Create a valid Account", + req: &pbs.CreateAccountRequest{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "name"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: ldap.Subtype.String(), + Attrs: &pb.Account_LdapAccountAttributes{ + LdapAccountAttributes: &pb.LdapAccountAttributes{ + LoginName: "valid-account", + }, + }, }, + }, + res: &pbs.CreateAccountResponse{ + Uri: fmt.Sprintf("accounts/%s_", globals.LdapAccountPrefix), Item: &pb.Account{ - Name: &wrapperspb.StringValue{Value: "new"}, - Description: &wrapperspb.StringValue{Value: "desc"}, - Type: "password", + AuthMethodId: am.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "name"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, + Version: 1, + Type: ldap.Subtype.String(), + Attrs: &pb.Account_LdapAccountAttributes{ + LdapAccountAttributes: &pb.LdapAccountAttributes{ + LoginName: "valid-account", + }, + }, + AuthorizedActions: ldapAuthorizedActions, }, }, - res: &pbs.UpdateAccountResponse{ + }, + { + name: "Create a valid Account without type defined", + req: &pbs.CreateAccountRequest{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Attrs: &pb.Account_LdapAccountAttributes{ + LdapAccountAttributes: &pb.LdapAccountAttributes{ + LoginName: "no type defined", + }, + }, + }, + }, + res: &pbs.CreateAccountResponse{ + Uri: fmt.Sprintf("accounts/%s_", globals.LdapAccountPrefix), + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, + Version: 1, + Type: ldap.Subtype.String(), + Attrs: &pb.Account_LdapAccountAttributes{ + LdapAccountAttributes: &pb.LdapAccountAttributes{ + LoginName: "no type defined", + }, + }, + AuthorizedActions: ldapAuthorizedActions, + }, + }, + }, + { + name: "Cant specify mismatching type", + req: &pbs.CreateAccountRequest{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Type: password.Subtype.String(), + Attrs: &pb.Account_LdapAccountAttributes{ + LdapAccountAttributes: &pb.LdapAccountAttributes{ + LoginName: "cant-specify-mismatching-type", + }, + }, + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "Doesn't match the parent resource's type", + }, + { + name: "Can't specify Id", + req: &pbs.CreateAccountRequest{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Id: globals.LdapAccountPrefix + "_notallowed", + Type: ldap.Subtype.String(), + Attrs: &pb.Account_LdapAccountAttributes{ + LdapAccountAttributes: &pb.LdapAccountAttributes{ + LoginName: "cant-specify-mismatching-type", + }, + }, + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "name: \"id\", desc: \"This is a read only field.\"", + }, + { + name: "Can't specify Created Time", + req: &pbs.CreateAccountRequest{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + CreatedTime: timestamppb.Now(), + Type: oidc.Subtype.String(), + Attrs: &pb.Account_LdapAccountAttributes{ + LdapAccountAttributes: &pb.LdapAccountAttributes{ + LoginName: "cant-specify-mismatching-type", + }, + }, + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "name: \"created_time\", desc: \"This is a read only field.\"", + }, + { + name: "Can't specify Update Time", + req: &pbs.CreateAccountRequest{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + UpdatedTime: timestamppb.Now(), + Type: ldap.Subtype.String(), + Attrs: &pb.Account_LdapAccountAttributes{ + LdapAccountAttributes: &pb.LdapAccountAttributes{ + LoginName: "cant-specify-mismatching-type", + }, + }, + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "name: \"updated_time\", desc: \"This is a read only field.\"", + }, + { + name: "Must specify login name", + req: &pbs.CreateAccountRequest{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Type: ldap.Subtype.String(), + Attrs: &pb.Account_LdapAccountAttributes{ + LdapAccountAttributes: &pb.LdapAccountAttributes{}, + }, + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "name: \"attributes.login_name\", desc: \"This is a required field for this type.", + }, + { + name: "Can't specify full name", + req: &pbs.CreateAccountRequest{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Type: ldap.Subtype.String(), + Attrs: &pb.Account_LdapAccountAttributes{ + LdapAccountAttributes: &pb.LdapAccountAttributes{ + LoginName: "cant-specify-full-name", + FullName: "something", + }, + }, + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "name: \"attributes.full_name\", desc: \"This is a read only field.\"", + }, + { + name: "Can't specify email", + req: &pbs.CreateAccountRequest{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Type: ldap.Subtype.String(), + Attrs: &pb.Account_LdapAccountAttributes{ + LdapAccountAttributes: &pb.LdapAccountAttributes{ + LoginName: "cant-specify-email", + Email: "something", + }, + }, + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "name: \"attributes.email\", desc: \"This is a read only field.\"", + }, + { + name: "Can't specify dn", + req: &pbs.CreateAccountRequest{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Type: ldap.Subtype.String(), + Attrs: &pb.Account_LdapAccountAttributes{ + LdapAccountAttributes: &pb.LdapAccountAttributes{ + LoginName: "cant-specify-dn", + Dn: "something", + }, + }, + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "name: \"attributes.dn\", desc: \"This is a read only field.\"", + }, + { + name: "Can't specify member of groups", + req: &pbs.CreateAccountRequest{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Type: ldap.Subtype.String(), + Attrs: &pb.Account_LdapAccountAttributes{ + LdapAccountAttributes: &pb.LdapAccountAttributes{ + LoginName: "cant-specify-member-of", + MemberOfGroups: []string{"something"}, + }, + }, + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "name: \"attributes.member_of_groups\", desc: \"This is a read only field.\"", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, gErr := s.CreateAccount(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req) + if tc.err != nil { + require.Error(gErr) + assert.True(errors.Is(gErr, tc.err), "CreateAccount(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) + require.NotEmpty(tc.errContains) + assert.Contains(gErr.Error(), tc.errContains) + return + } + require.NoError(gErr) + if got != nil { + assert.Contains(got.GetUri(), tc.res.Uri) + assert.True(strings.HasPrefix(got.GetItem().GetId(), globals.LdapAccountPrefix+"_")) + // Clear all values which are hard to compare against. + got.Uri, tc.res.Uri = "", "" + got.Item.Id, tc.res.Item.Id = "", "" + got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil + } + assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "CreateAccount(%q) got response %q, wanted %q", tc.req, got, tc.res) + }) + } +} + +func TestUpdatePassword(t *testing.T) { + ctx := context.TODO() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrap := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrap) + pwRepoFn := func() (*password.Repository, error) { + return password.NewRepository(rw, rw, kms) + } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(ctx, rw, rw, kms) + } + iamRepoFn := func() (*iam.Repository, error) { + return iam.NewRepository(rw, rw, kms) + } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kms) + } + + o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) + am := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0] + tested, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn) + require.NoError(t, err, "Error when getting new accounts service.") + + defaultScopeInfo := &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()} + defaultAttributes := &pb.Account_PasswordAccountAttributes{ + &pb.PasswordAccountAttributes{ + LoginName: "default", + }, + } + modifiedAttributes := &pb.Account_PasswordAccountAttributes{ + &pb.PasswordAccountAttributes{ + LoginName: "modified", + }, + } + + freshAccount := func(t *testing.T) (*pb.Account, func()) { + t.Helper() + acc, err := tested.CreateAccount(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), + &pbs.CreateAccountRequest{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Name: wrapperspb.String("default"), + Description: wrapperspb.String("default"), + Type: "password", + Attrs: defaultAttributes, + }, + }, + ) + require.NoError(t, err) + + clean := func() { + _, err := tested.DeleteAccount(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), + &pbs.DeleteAccountRequest{Id: acc.GetItem().GetId()}) + require.NoError(t, err) + } + + return acc.GetItem(), clean + } + + cases := []struct { + name string + req *pbs.UpdateAccountRequest + res *pbs.UpdateAccountResponse + err error + }{ + { + name: "Update an Existing AuthMethod", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{globals.NameField, globals.DescriptionField}, + }, + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: "password", + }, + }, + res: &pbs.UpdateAccountResponse{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: "password", + Attrs: defaultAttributes, + Scope: defaultScopeInfo, + AuthorizedActions: pwAuthorizedActions, + }, + }, + }, + { + name: "Multiple Paths in single string", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name,description"}, + }, + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: "password", + }, + }, + res: &pbs.UpdateAccountResponse{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: "password", + Attrs: defaultAttributes, + Scope: defaultScopeInfo, + AuthorizedActions: pwAuthorizedActions, + }, + }, + }, + { + name: "No Update Mask", + req: &pbs.UpdateAccountRequest{ + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: "updated name"}, + Description: &wrapperspb.StringValue{Value: "updated desc"}, + Attrs: modifiedAttributes, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Cant change type", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{globals.NameField}, + }, + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: ""}, + Type: "oidc", + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "No Paths in Mask", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{Paths: []string{}}, + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: "updated name"}, + Description: &wrapperspb.StringValue{Value: "updated desc"}, + Attrs: modifiedAttributes, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Only non-existant paths in Mask", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{Paths: []string{"nonexistant_field"}}, + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: "updated name"}, + Description: &wrapperspb.StringValue{Value: "updated desc"}, + Attrs: modifiedAttributes, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Unset Name", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{globals.NameField}, + }, + Item: &pb.Account{ + Description: &wrapperspb.StringValue{Value: "ignored"}, + Attrs: modifiedAttributes, + }, + }, + res: &pbs.UpdateAccountResponse{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Description: &wrapperspb.StringValue{Value: "default"}, + Type: "password", + Attrs: defaultAttributes, + Scope: defaultScopeInfo, + AuthorizedActions: pwAuthorizedActions, + }, + }, + }, + { + name: "Update Only Name", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{globals.NameField}, + }, + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: "updated"}, + Description: &wrapperspb.StringValue{Value: "ignored"}, + Attrs: modifiedAttributes, + }, + }, + res: &pbs.UpdateAccountResponse{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "updated"}, + Description: &wrapperspb.StringValue{Value: "default"}, + Type: "password", + Attrs: defaultAttributes, + Scope: defaultScopeInfo, + AuthorizedActions: pwAuthorizedActions, + }, + }, + }, + { + name: "Update Only Description", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{globals.DescriptionField}, + }, + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: "ignored"}, + Description: &wrapperspb.StringValue{Value: "notignored"}, + Attrs: modifiedAttributes, + }, + }, + res: &pbs.UpdateAccountResponse{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "default"}, + Description: &wrapperspb.StringValue{Value: "notignored"}, + Type: "password", + Attrs: defaultAttributes, + Scope: defaultScopeInfo, + AuthorizedActions: pwAuthorizedActions, + }, + }, + }, + { + name: "Update Only LoginName", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.login_name"}, + }, + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: "ignored"}, + Description: &wrapperspb.StringValue{Value: "ignored"}, + Attrs: modifiedAttributes, + }, + }, + res: &pbs.UpdateAccountResponse{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "default"}, + Description: &wrapperspb.StringValue{Value: "default"}, + Type: "password", + Attrs: modifiedAttributes, + Scope: defaultScopeInfo, + AuthorizedActions: pwAuthorizedActions, + }, + }, + }, + { + name: "Update a Non Existing Old ID Account", + req: &pbs.UpdateAccountRequest{ + Id: globals.PasswordAccountPreviousPrefix + "_DoesntExis", + UpdateMask: &field_mask.FieldMask{ + Paths: []string{globals.DescriptionField}, + }, + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + }, + }, + err: handlers.ApiErrorWithCode(codes.NotFound), + }, + { + name: "Update a Non Existing New ID Account", + req: &pbs.UpdateAccountRequest{ + Id: globals.PasswordAccountPrefix + "_DoesntExis", + UpdateMask: &field_mask.FieldMask{ + Paths: []string{globals.DescriptionField}, + }, + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + }, + }, + err: handlers.ApiErrorWithCode(codes.NotFound), + }, + { + name: "Cant change Id", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"id"}, + }, + Item: &pb.Account{ + Id: globals.PasswordAccountPrefix + "_somethinge", + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "new desc"}, + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Cant specify Created Time", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"created_time"}, + }, + Item: &pb.Account{ + CreatedTime: timestamppb.Now(), + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Cant specify Updated Time", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"updated_time"}, + }, + Item: &pb.Account{ + UpdatedTime: timestamppb.Now(), + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Cant specify Type", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"type"}, + }, + Item: &pb.Account{ + Type: "oidc", + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + acc, cleanup := freshAccount(t) + defer cleanup() + + tc.req.Item.Version = 1 + + if tc.req.GetId() == "" { + tc.req.Id = acc.GetId() + } + + if tc.res != nil && tc.res.Item != nil { + tc.res.Item.Id = acc.GetId() + tc.res.Item.CreatedTime = acc.GetCreatedTime() + } + + got, gErr := tested.UpdateAccount(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req) + if tc.err != nil { + require.Error(gErr) + assert.True(errors.Is(gErr, tc.err), "UpdateAccount(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) + } else { + require.NoError(gErr) + } + + if tc.res == nil { + require.Nil(got) + } + + if got != nil { + assert.NotNilf(tc.res, "Expected UpdateAccount response to be nil, but was %v", got) + gotUpdateTime := got.GetItem().GetUpdatedTime() + require.NoError(err, "Error converting proto to timestamp") + + created := acc.GetCreatedTime() + require.NoError(err, "Error converting proto to timestamp") + + // Verify it is a auth_method updated after it was created + assert.True(gotUpdateTime.AsTime().After(created.AsTime()), "Updated account should have been updated after it's creation. Was updated %v, which is after %v", gotUpdateTime, created) + + // Clear all values which are hard to compare against. + got.Item.UpdatedTime, tc.res.Item.UpdatedTime = nil, nil + + assert.EqualValues(2, got.Item.Version) + tc.res.Item.Version = 2 + } + assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "UpdateAccount(%q) got response %q, wanted %q", tc.req, got, tc.res) + }) + } +} + +func TestUpdateOidc(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrap := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrap) + pwRepoFn := func() (*password.Repository, error) { + return password.NewRepository(rw, rw, kmsCache) + } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(ctx, rw, rw, kmsCache) + } + iamRepoFn := func() (*iam.Repository, error) { + return iam.NewRepository(rw, rw, kmsCache) + } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kmsCache) + } + + o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) + + databaseWrapper, err := kmsCache.GetWrapper(ctx, o.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + am := oidc.TestAuthMethod( + t, conn, databaseWrapper, o.PublicId, oidc.ActivePrivateState, + "alice-rp", "fido", + oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.alice.com")[0]), + oidc.WithSigningAlgs(oidc.RS256), + oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0])) + + tested, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn) + require.NoError(t, err, "Error when getting new auth_method service.") + + defaultScopeInfo := &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()} + defaultAttributes := &pb.Account_OidcAccountAttributes{ + &pb.OidcAccountAttributes{ + Issuer: "https://www.alice.com", + Subject: "test-subject", + }, + } + modifiedAttributes := &pb.Account_OidcAccountAttributes{ + &pb.OidcAccountAttributes{ + Issuer: "https://www.changed.com", + Subject: "changed", + }, + } + + freshAccount := func(t *testing.T) (*oidc.Account, func()) { + t.Helper() + acc := oidc.TestAccount(t, conn, am, "test-subject", oidc.WithName("default"), oidc.WithDescription("default")) + + clean := func() { + _, err := tested.DeleteAccount(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), + &pbs.DeleteAccountRequest{Id: acc.GetPublicId()}) + require.NoError(t, err) + } + + return acc, clean + } + + cases := []struct { + name string + req *pbs.UpdateAccountRequest + res *pbs.UpdateAccountResponse + err error + }{ + { + name: "Update an Existing AuthMethod", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{globals.NameField, globals.DescriptionField}, + }, + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: oidc.Subtype.String(), + }, + }, + res: &pbs.UpdateAccountResponse{ Item: &pb.Account{ AuthMethodId: am.GetPublicId(), Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, - Type: "password", + Type: oidc.Subtype.String(), Attrs: defaultAttributes, Scope: defaultScopeInfo, - AuthorizedActions: pwAuthorizedActions, + AuthorizedActions: oidcAuthorizedActions, }, }, }, @@ -1247,7 +2169,7 @@ func TestUpdatePassword(t *testing.T) { Item: &pb.Account{ Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, - Type: "password", + Type: oidc.Subtype.String(), }, }, res: &pbs.UpdateAccountResponse{ @@ -1255,10 +2177,10 @@ func TestUpdatePassword(t *testing.T) { AuthMethodId: am.GetPublicId(), Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, - Type: "password", + Type: oidc.Subtype.String(), Attrs: defaultAttributes, Scope: defaultScopeInfo, - AuthorizedActions: pwAuthorizedActions, + AuthorizedActions: oidcAuthorizedActions, }, }, }, @@ -1325,10 +2247,10 @@ func TestUpdatePassword(t *testing.T) { Item: &pb.Account{ AuthMethodId: am.GetPublicId(), Description: &wrapperspb.StringValue{Value: "default"}, - Type: "password", + Type: oidc.Subtype.String(), Attrs: defaultAttributes, Scope: defaultScopeInfo, - AuthorizedActions: pwAuthorizedActions, + AuthorizedActions: oidcAuthorizedActions, }, }, }, @@ -1349,10 +2271,10 @@ func TestUpdatePassword(t *testing.T) { AuthMethodId: am.GetPublicId(), Name: &wrapperspb.StringValue{Value: "updated"}, Description: &wrapperspb.StringValue{Value: "default"}, - Type: "password", + Type: oidc.Subtype.String(), Attrs: defaultAttributes, Scope: defaultScopeInfo, - AuthorizedActions: pwAuthorizedActions, + AuthorizedActions: oidcAuthorizedActions, }, }, }, @@ -1373,15 +2295,15 @@ func TestUpdatePassword(t *testing.T) { AuthMethodId: am.GetPublicId(), Name: &wrapperspb.StringValue{Value: "default"}, Description: &wrapperspb.StringValue{Value: "notignored"}, - Type: "password", + Type: oidc.Subtype.String(), Attrs: defaultAttributes, Scope: defaultScopeInfo, - AuthorizedActions: pwAuthorizedActions, + AuthorizedActions: oidcAuthorizedActions, }, }, }, { - name: "Update Only LoginName", + name: "Update LoginName", req: &pbs.UpdateAccountRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"attributes.login_name"}, @@ -1392,34 +2314,10 @@ func TestUpdatePassword(t *testing.T) { Attrs: modifiedAttributes, }, }, - res: &pbs.UpdateAccountResponse{ - Item: &pb.Account{ - AuthMethodId: am.GetPublicId(), - Name: &wrapperspb.StringValue{Value: "default"}, - Description: &wrapperspb.StringValue{Value: "default"}, - Type: "password", - Attrs: modifiedAttributes, - Scope: defaultScopeInfo, - AuthorizedActions: pwAuthorizedActions, - }, - }, - }, - { - name: "Update a Non Existing Old ID Account", - req: &pbs.UpdateAccountRequest{ - Id: globals.PasswordAccountPreviousPrefix + "_DoesntExis", - UpdateMask: &field_mask.FieldMask{ - Paths: []string{globals.DescriptionField}, - }, - Item: &pb.Account{ - Name: &wrapperspb.StringValue{Value: "new"}, - Description: &wrapperspb.StringValue{Value: "desc"}, - }, - }, - err: handlers.ApiErrorWithCode(codes.NotFound), + err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { - name: "Update a Non Existing New ID Account", + name: "Update a Non Existing Account", req: &pbs.UpdateAccountRequest{ Id: globals.PasswordAccountPrefix + "_DoesntExis", UpdateMask: &field_mask.FieldMask{ @@ -1486,6 +2384,32 @@ func TestUpdatePassword(t *testing.T) { res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, + { + name: "Update Issuer", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.issuer"}, + }, + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: "ignored"}, + Attrs: modifiedAttributes, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Update Subject", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.subject"}, + }, + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: "ignored"}, + Attrs: modifiedAttributes, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -1496,20 +2420,18 @@ func TestUpdatePassword(t *testing.T) { tc.req.Item.Version = 1 if tc.req.GetId() == "" { - tc.req.Id = acc.GetId() + tc.req.Id = acc.GetPublicId() } if tc.res != nil && tc.res.Item != nil { - tc.res.Item.Id = acc.GetId() - tc.res.Item.CreatedTime = acc.GetCreatedTime() + tc.res.Item.Id = acc.GetPublicId() + tc.res.Item.CreatedTime = acc.GetCreateTime().GetTimestamp() } got, gErr := tested.UpdateAccount(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "UpdateAccount(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) - } else { - require.NoError(gErr) } if tc.res == nil { @@ -1521,7 +2443,7 @@ func TestUpdatePassword(t *testing.T) { gotUpdateTime := got.GetItem().GetUpdatedTime() require.NoError(err, "Error converting proto to timestamp") - created := acc.GetCreatedTime() + created := acc.GetCreateTime().GetTimestamp() require.NoError(err, "Error converting proto to timestamp") // Verify it is a auth_method updated after it was created @@ -1538,7 +2460,7 @@ func TestUpdatePassword(t *testing.T) { } } -func TestUpdateOidc(t *testing.T) { +func TestUpdateLdap(t *testing.T) { ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) @@ -1553,38 +2475,29 @@ func TestUpdateOidc(t *testing.T) { iamRepoFn := func() (*iam.Repository, error) { return iam.NewRepository(rw, rw, kmsCache) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kmsCache) + } o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) databaseWrapper, err := kmsCache.GetWrapper(ctx, o.PublicId, kms.KeyPurposeDatabase) require.NoError(t, err) - am := oidc.TestAuthMethod( - t, conn, databaseWrapper, o.PublicId, oidc.ActivePrivateState, - "alice-rp", "fido", - oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.alice.com")[0]), - oidc.WithSigningAlgs(oidc.RS256), - oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0])) + am := ldap.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, []string{"ldaps://ldap"}) - tested, err := accounts.NewService(pwRepoFn, oidcRepoFn) + tested, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn) require.NoError(t, err, "Error when getting new auth_method service.") defaultScopeInfo := &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()} - defaultAttributes := &pb.Account_OidcAccountAttributes{ - &pb.OidcAccountAttributes{ - Issuer: "https://www.alice.com", - Subject: "test-subject", - }, - } - modifiedAttributes := &pb.Account_OidcAccountAttributes{ - &pb.OidcAccountAttributes{ - Issuer: "https://www.changed.com", - Subject: "changed", + defaultAttributes := &pb.Account_LdapAccountAttributes{ + LdapAccountAttributes: &pb.LdapAccountAttributes{ + LoginName: "test-login", }, } - freshAccount := func(t *testing.T) (*oidc.Account, func()) { + freshAccount := func(t *testing.T) (*ldap.Account, func()) { t.Helper() - acc := oidc.TestAccount(t, conn, am, "test-subject", oidc.WithName("default"), oidc.WithDescription("default")) + acc := ldap.TestAccount(t, conn, am, "test-login", ldap.WithName(ctx, "default"), ldap.WithDescription(ctx, "default")) clean := func() { _, err := tested.DeleteAccount(requestauth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), @@ -1596,13 +2509,14 @@ func TestUpdateOidc(t *testing.T) { } cases := []struct { - name string - req *pbs.UpdateAccountRequest - res *pbs.UpdateAccountResponse - err error + name string + req *pbs.UpdateAccountRequest + res *pbs.UpdateAccountResponse + err error + errContains string }{ { - name: "Update an Existing AuthMethod", + name: "update-existing", req: &pbs.UpdateAccountRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{globals.NameField, globals.DescriptionField}, @@ -1610,7 +2524,7 @@ func TestUpdateOidc(t *testing.T) { Item: &pb.Account{ Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, - Type: oidc.Subtype.String(), + Type: ldap.Subtype.String(), }, }, res: &pbs.UpdateAccountResponse{ @@ -1618,10 +2532,10 @@ func TestUpdateOidc(t *testing.T) { AuthMethodId: am.GetPublicId(), Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, - Type: oidc.Subtype.String(), + Type: ldap.Subtype.String(), Attrs: defaultAttributes, Scope: defaultScopeInfo, - AuthorizedActions: oidcAuthorizedActions, + AuthorizedActions: ldapAuthorizedActions, }, }, }, @@ -1634,7 +2548,7 @@ func TestUpdateOidc(t *testing.T) { Item: &pb.Account{ Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, - Type: oidc.Subtype.String(), + Type: ldap.Subtype.String(), }, }, res: &pbs.UpdateAccountResponse{ @@ -1642,10 +2556,10 @@ func TestUpdateOidc(t *testing.T) { AuthMethodId: am.GetPublicId(), Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, - Type: oidc.Subtype.String(), + Type: ldap.Subtype.String(), Attrs: defaultAttributes, Scope: defaultScopeInfo, - AuthorizedActions: oidcAuthorizedActions, + AuthorizedActions: ldapAuthorizedActions, }, }, }, @@ -1655,10 +2569,10 @@ func TestUpdateOidc(t *testing.T) { Item: &pb.Account{ Name: &wrapperspb.StringValue{Value: "updated name"}, Description: &wrapperspb.StringValue{Value: "updated desc"}, - Attrs: modifiedAttributes, }, }, - err: handlers.ApiErrorWithCode(codes.InvalidArgument), + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "UpdateMask not provided but is required to update this resource", }, { name: "Cant change type", @@ -1671,7 +2585,8 @@ func TestUpdateOidc(t *testing.T) { Type: "oidc", }, }, - err: handlers.ApiErrorWithCode(codes.InvalidArgument), + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "Cannot modify the resource type", }, { name: "No Paths in Mask", @@ -1680,22 +2595,22 @@ func TestUpdateOidc(t *testing.T) { Item: &pb.Account{ Name: &wrapperspb.StringValue{Value: "updated name"}, Description: &wrapperspb.StringValue{Value: "updated desc"}, - Attrs: modifiedAttributes, }, }, - err: handlers.ApiErrorWithCode(codes.InvalidArgument), + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "missing mask", }, { - name: "Only non-existant paths in Mask", + name: "Only non-existent paths in Mask", req: &pbs.UpdateAccountRequest{ UpdateMask: &field_mask.FieldMask{Paths: []string{"nonexistant_field"}}, Item: &pb.Account{ Name: &wrapperspb.StringValue{Value: "updated name"}, Description: &wrapperspb.StringValue{Value: "updated desc"}, - Attrs: modifiedAttributes, }, }, - err: handlers.ApiErrorWithCode(codes.InvalidArgument), + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "No valid fields included in the update mask", }, { name: "Unset Name", @@ -1705,17 +2620,16 @@ func TestUpdateOidc(t *testing.T) { }, Item: &pb.Account{ Description: &wrapperspb.StringValue{Value: "ignored"}, - Attrs: modifiedAttributes, }, }, res: &pbs.UpdateAccountResponse{ Item: &pb.Account{ AuthMethodId: am.GetPublicId(), Description: &wrapperspb.StringValue{Value: "default"}, - Type: oidc.Subtype.String(), + Type: ldap.Subtype.String(), Attrs: defaultAttributes, Scope: defaultScopeInfo, - AuthorizedActions: oidcAuthorizedActions, + AuthorizedActions: ldapAuthorizedActions, }, }, }, @@ -1728,7 +2642,6 @@ func TestUpdateOidc(t *testing.T) { Item: &pb.Account{ Name: &wrapperspb.StringValue{Value: "updated"}, Description: &wrapperspb.StringValue{Value: "ignored"}, - Attrs: modifiedAttributes, }, }, res: &pbs.UpdateAccountResponse{ @@ -1736,10 +2649,10 @@ func TestUpdateOidc(t *testing.T) { AuthMethodId: am.GetPublicId(), Name: &wrapperspb.StringValue{Value: "updated"}, Description: &wrapperspb.StringValue{Value: "default"}, - Type: oidc.Subtype.String(), + Type: ldap.Subtype.String(), Attrs: defaultAttributes, Scope: defaultScopeInfo, - AuthorizedActions: oidcAuthorizedActions, + AuthorizedActions: ldapAuthorizedActions, }, }, }, @@ -1752,7 +2665,6 @@ func TestUpdateOidc(t *testing.T) { Item: &pb.Account{ Name: &wrapperspb.StringValue{Value: "ignored"}, Description: &wrapperspb.StringValue{Value: "notignored"}, - Attrs: modifiedAttributes, }, }, res: &pbs.UpdateAccountResponse{ @@ -1760,10 +2672,10 @@ func TestUpdateOidc(t *testing.T) { AuthMethodId: am.GetPublicId(), Name: &wrapperspb.StringValue{Value: "default"}, Description: &wrapperspb.StringValue{Value: "notignored"}, - Type: oidc.Subtype.String(), + Type: ldap.Subtype.String(), Attrs: defaultAttributes, Scope: defaultScopeInfo, - AuthorizedActions: oidcAuthorizedActions, + AuthorizedActions: ldapAuthorizedActions, }, }, }, @@ -1776,15 +2688,15 @@ func TestUpdateOidc(t *testing.T) { Item: &pb.Account{ Name: &wrapperspb.StringValue{Value: "ignored"}, Description: &wrapperspb.StringValue{Value: "ignored"}, - Attrs: modifiedAttributes, }, }, - err: handlers.ApiErrorWithCode(codes.InvalidArgument), + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "Field cannot be updated.", }, { name: "Update a Non Existing Account", req: &pbs.UpdateAccountRequest{ - Id: globals.PasswordAccountPrefix + "_DoesntExis", + Id: globals.LdapAccountPrefix + "_DoesntExis", UpdateMask: &field_mask.FieldMask{ Paths: []string{globals.DescriptionField}, }, @@ -1793,7 +2705,8 @@ func TestUpdateOidc(t *testing.T) { Description: &wrapperspb.StringValue{Value: "desc"}, }, }, - err: handlers.ApiErrorWithCode(codes.NotFound), + err: handlers.ApiErrorWithCode(codes.NotFound), + errContains: "Resource not found.", }, { name: "Cant change Id", @@ -1802,13 +2715,14 @@ func TestUpdateOidc(t *testing.T) { Paths: []string{"id"}, }, Item: &pb.Account{ - Id: globals.PasswordAccountPrefix + "_somethinge", + Id: globals.LdapAccountPrefix + "_somethinge", Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "new desc"}, }, }, - res: nil, - err: handlers.ApiErrorWithCode(codes.InvalidArgument), + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "name: \"id\", desc: \"This is a read only field and cannot be specified in an update request.", }, { name: "Cant specify Created Time", @@ -1820,8 +2734,9 @@ func TestUpdateOidc(t *testing.T) { CreatedTime: timestamppb.Now(), }, }, - res: nil, - err: handlers.ApiErrorWithCode(codes.InvalidArgument), + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "name: \"created_time\", desc: \"This is a read only field and cannot be specified in an update request.", }, { name: "Cant specify Updated Time", @@ -1833,8 +2748,9 @@ func TestUpdateOidc(t *testing.T) { UpdatedTime: timestamppb.Now(), }, }, - res: nil, - err: handlers.ApiErrorWithCode(codes.InvalidArgument), + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "name: \"updated_time\", desc: \"This is a read only field and cannot be specified in an update request.", }, { name: "Cant specify Type", @@ -1846,34 +2762,22 @@ func TestUpdateOidc(t *testing.T) { Type: "oidc", }, }, - res: nil, - err: handlers.ApiErrorWithCode(codes.InvalidArgument), - }, - { - name: "Update Issuer", - req: &pbs.UpdateAccountRequest{ - UpdateMask: &field_mask.FieldMask{ - Paths: []string{"attributes.issuer"}, - }, - Item: &pb.Account{ - Name: &wrapperspb.StringValue{Value: "ignored"}, - Attrs: modifiedAttributes, - }, - }, - err: handlers.ApiErrorWithCode(codes.InvalidArgument), + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "name: \"type\", desc: \"Cannot modify the resource type.", }, { - name: "Update Subject", + name: "Update Login name", req: &pbs.UpdateAccountRequest{ UpdateMask: &field_mask.FieldMask{ - Paths: []string{"attributes.subject"}, + Paths: []string{"attributes.login_name"}, }, Item: &pb.Account{ - Name: &wrapperspb.StringValue{Value: "ignored"}, - Attrs: modifiedAttributes, + Name: &wrapperspb.StringValue{Value: "ignored"}, }, }, - err: handlers.ApiErrorWithCode(codes.InvalidArgument), + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "name: \"attributes.login_name\", desc: \"Field cannot be updated.", }, } for _, tc := range cases { @@ -1897,8 +2801,13 @@ func TestUpdateOidc(t *testing.T) { if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "UpdateAccount(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) + require.NotEmpty(tc.errContains) + assert.Contains(gErr.Error(), tc.errContains) + return } + require.NoError(gErr) + if tc.res == nil { require.Nil(got) } @@ -1940,9 +2849,12 @@ func TestSetPassword(t *testing.T) { iamRepoFn := func() (*iam.Repository, error) { return iam.NewRepository(rw, rw, kms) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kms) + } o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) - tested, err := accounts.NewService(pwRepoFn, oidcRepoFn) + tested, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn) require.NoError(t, err, "Error when getting new auth_method service.") createAccount := func(t *testing.T, pw string) *pb.Account { @@ -2079,9 +2991,12 @@ func TestChangePassword(t *testing.T) { iamRepoFn := func() (*iam.Repository, error) { return iam.NewRepository(rw, rw, kms) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kms) + } o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) - tested, err := accounts.NewService(pwRepoFn, oidcRepoFn) + tested, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn) require.NoError(t, err, "Error when getting new auth_method service.") createAccount := func(t *testing.T, pw string) *pb.Account { diff --git a/internal/daemon/controller/handlers/accounts/validate_test.go b/internal/daemon/controller/handlers/accounts/validate_test.go index 56a292f7b2..966b27304f 100644 --- a/internal/daemon/controller/handlers/accounts/validate_test.go +++ b/internal/daemon/controller/handlers/accounts/validate_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" @@ -52,6 +53,14 @@ func TestValidateCreateRequest(t *testing.T) { }, errContains: fieldError(typeField, "Doesn't match the parent resource's type."), }, + { + name: "mismatched pw authmethod ldap type", + item: &pb.Account{ + Type: ldap.Subtype.String(), + AuthMethodId: globals.PasswordAuthMethodPrefix + "_1234567890", + }, + errContains: fieldError(typeField, "Doesn't match the parent resource's type."), + }, { name: "missing oidc attributes", item: &pb.Account{ @@ -93,6 +102,69 @@ func TestValidateCreateRequest(t *testing.T) { }, errContains: fieldError(emailClaimField, "This is a read only field."), }, + { + name: "missing ldap attributes", + item: &pb.Account{ + Type: ldap.Subtype.String(), + AuthMethodId: globals.LdapAuthMethodPrefix + "_1234567890", + }, + errContains: fieldError(attributesField, "This is a required field."), + }, + { + name: "missing login name for ldap type", + item: &pb.Account{ + Type: ldap.Subtype.String(), + AuthMethodId: globals.LdapAuthMethodPrefix + "_1234567890", + Attrs: &pb.Account_LdapAccountAttributes{ + LdapAccountAttributes: &pb.LdapAccountAttributes{}, + }, + }, + errContains: fieldError(loginAttrField, "This is a required field for this type."), + }, + { + name: "read only full name attr field", + item: &pb.Account{ + Type: ldap.Subtype.String(), + AuthMethodId: globals.LdapAuthMethodPrefix + "_1234567890", + Attrs: &pb.Account_LdapAccountAttributes{ + LdapAccountAttributes: &pb.LdapAccountAttributes{FullName: "something"}, + }, + }, + errContains: fieldError(nameAttrField, "This is a read only field."), + }, + { + name: "read only email attr field", + item: &pb.Account{ + Type: ldap.Subtype.String(), + AuthMethodId: globals.LdapAuthMethodPrefix + "_1234567890", + Attrs: &pb.Account_LdapAccountAttributes{ + LdapAccountAttributes: &pb.LdapAccountAttributes{Email: "something"}, + }, + }, + errContains: fieldError(emailAttrField, "This is a read only field."), + }, + { + name: "read only dn attr field", + item: &pb.Account{ + Type: ldap.Subtype.String(), + AuthMethodId: globals.LdapAuthMethodPrefix + "_1234567890", + Attrs: &pb.Account_LdapAccountAttributes{ + LdapAccountAttributes: &pb.LdapAccountAttributes{Dn: "something"}, + }, + }, + errContains: fieldError(dnAttrField, "This is a read only field."), + }, + { + name: "read only member of attr field", + item: &pb.Account{ + Type: ldap.Subtype.String(), + AuthMethodId: globals.LdapAuthMethodPrefix + "_1234567890", + Attrs: &pb.Account_LdapAccountAttributes{ + LdapAccountAttributes: &pb.LdapAccountAttributes{MemberOfGroups: []string{"something"}}, + }, + }, + errContains: fieldError(memberOfAttrField, "This is a read only field."), + }, { name: "missing password attributes", item: &pb.Account{ @@ -134,6 +206,16 @@ func TestValidateCreateRequest(t *testing.T) { }, }, }, + { + name: "no ldap errors", + item: &pb.Account{ + Type: ldap.Subtype.String(), + AuthMethodId: globals.LdapAuthMethodPrefix + "_1234567890", + Attrs: &pb.Account_LdapAccountAttributes{ + LdapAccountAttributes: &pb.LdapAccountAttributes{LoginName: "no oidc errors"}, + }, + }, + }, } for _, tc := range cases { tc := tc diff --git a/internal/daemon/controller/handlers/authmethods/authmethod_service.go b/internal/daemon/controller/handlers/authmethods/authmethod_service.go index cdd446155d..6143d35c4b 100644 --- a/internal/daemon/controller/handlers/authmethods/authmethod_service.go +++ b/internal/daemon/controller/handlers/authmethods/authmethod_service.go @@ -5,12 +5,14 @@ package authmethods import ( "context" + "encoding/base64" "fmt" "net/url" "strings" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth" + "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/authtoken" @@ -86,12 +88,13 @@ type Service struct { oidcRepoFn common.OidcAuthRepoFactory iamRepoFn common.IamRepoFactory atRepoFn common.AuthTokenRepoFactory + ldapRepoFn common.LdapAuthRepoFactory } var _ pbs.AuthMethodServiceServer = (*Service)(nil) // NewService returns a auth method service which handles auth method related requests to boundary. -func NewService(kms *kms.Kms, pwRepoFn common.PasswordAuthRepoFactory, oidcRepoFn common.OidcAuthRepoFactory, iamRepoFn common.IamRepoFactory, atRepoFn common.AuthTokenRepoFactory, opt ...handlers.Option) (Service, error) { +func NewService(kms *kms.Kms, pwRepoFn common.PasswordAuthRepoFactory, oidcRepoFn common.OidcAuthRepoFactory, iamRepoFn common.IamRepoFactory, atRepoFn common.AuthTokenRepoFactory, ldapRepoFn common.LdapAuthRepoFactory, opt ...handlers.Option) (Service, error) { const op = "authmethods.NewService" if kms == nil { return Service{}, errors.NewDeprecated(errors.InvalidParameter, op, "missing kms") @@ -102,13 +105,16 @@ func NewService(kms *kms.Kms, pwRepoFn common.PasswordAuthRepoFactory, oidcRepoF if oidcRepoFn == nil { return Service{}, fmt.Errorf("nil oidc repository provided") } + if ldapRepoFn == nil { + return Service{}, fmt.Errorf("nil ldap repository provided") + } if iamRepoFn == nil { return Service{}, errors.NewDeprecated(errors.InvalidParameter, op, "missing iam repository") } if atRepoFn == nil { return Service{}, fmt.Errorf("nil auth token repository provided") } - s := Service{kms: kms, pwRepoFn: pwRepoFn, oidcRepoFn: oidcRepoFn, iamRepoFn: iamRepoFn, atRepoFn: atRepoFn} + s := Service{kms: kms, pwRepoFn: pwRepoFn, oidcRepoFn: oidcRepoFn, iamRepoFn: iamRepoFn, atRepoFn: atRepoFn, ldapRepoFn: ldapRepoFn} return s, nil } @@ -427,6 +433,10 @@ func (s Service) Authenticate(ctx context.Context, req *pbs.AuthenticateRequest) if err := validateAuthenticateOidcRequest(req); err != nil { return nil, err } + case ldap.Subtype: + if err := validateAuthenticateLdapRequest(req); err != nil { + return nil, err + } } authResults := s.authResult(ctx, req.GetAuthMethodId(), action.Authenticate) @@ -440,6 +450,8 @@ func (s Service) Authenticate(ctx context.Context, req *pbs.AuthenticateRequest) case oidc.Subtype: return s.authenticateOidc(ctx, req, &authResults) + case ldap.Subtype: + return s.authenticateLdap(ctx, req, &authResults) } return nil, errors.New(ctx, errors.Internal, op, "Invalid auth method subtype not caught in validation function.") } @@ -462,6 +474,13 @@ func (s Service) getFromRepo(ctx context.Context, id string) (auth.AuthMethod, e } am, lookupErr = repo.LookupAuthMethod(ctx, id) + case ldap.Subtype: + repo, err := s.ldapRepoFn() + if err != nil { + return nil, err + } + am, lookupErr = repo.LookupAuthMethod(ctx, id) + default: return nil, handlers.NotFoundErrorf("Unrecognized id.") } @@ -511,6 +530,19 @@ func (s Service) listFromRepo(ctx context.Context, scopeIds []string, authResult for _, item := range pl { outUl = append(outUl, item) } + + ldapRepo, err := s.ldapRepoFn() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + ll, err := ldapRepo.ListAuthMethods(ctx, scopeIds, ldap.WithUnauthenticatedUser(ctx, reqCtx.UserId == globals.AnonymousUserId)) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + for _, item := range ll { + outUl = append(outUl, item) + } + return outUl, nil } @@ -536,6 +568,15 @@ func (s Service) createInRepo(ctx context.Context, scopeId string, item *pb.Auth return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create auth method but no error returned from repository.") } out = am + case ldap.Subtype: + am, err := s.createLdapInRepo(ctx, scopeId, item) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + if am == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create auth method but no error returned from repository.") + } + out = am } return out, nil } @@ -567,6 +608,24 @@ func (s Service) updateInRepo(ctx context.Context, scopeId string, req *pbs.Upda } am = oam dryRun = dr + + case ldap.Subtype: + lam, err := s.updateLdapInRepo(ctx, scopeId, req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem()) + if err != nil { + _, apiErr := err.(*handlers.ApiError) + switch { + case apiErr: + return nil, false, err + case errors.Match(errors.T(errors.InvalidParameter), err): + return nil, false, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, err.Error()) + default: + return nil, false, errors.Wrap(ctx, err, op) + } + } + if lam == nil { + return nil, false, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to update auth method but no error returned from repository.") + } + am = lam } return am, dryRun, nil @@ -590,6 +649,14 @@ func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool, return false, errors.Wrap(ctx, err, op) } rows, dErr = repo.DeleteAuthMethod(ctx, id) + case ldap.Subtype: + repo, err := s.ldapRepoFn() + if err != nil { + return false, errors.Wrap(ctx, err, op) + } + rows, dErr = repo.DeleteAuthMethod(ctx, id) + default: + return false, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("invalid auth method subtype: %q", subtypes.SubtypeFromId(domain, id))) } if dErr != nil { @@ -697,6 +764,22 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) reque return res } authMeth = am + case ldap.Subtype: + repo, err := s.ldapRepoFn() + if err != nil { + res.Error = err + return res + } + am, err := repo.LookupAuthMethod(ctx, id) + if err != nil { + res.Error = err + return res + } + if am == nil { + res.Error = handlers.NotFoundError() + return res + } + authMeth = am default: res.Error = errors.New(ctx, errors.InvalidPublicId, op, "unrecognized auth method type") return res @@ -801,6 +884,60 @@ func toAuthMethodProto(ctx context.Context, in auth.AuthMethod, opt ...handlers. out.Attrs = &pb.AuthMethod_OidcAuthMethodsAttributes{ OidcAuthMethodsAttributes: attrs, } + case *ldap.AuthMethod: + if outputFields.Has(globals.TypeField) { + out.Type = ldap.Subtype.String() + } + if !outputFields.Has(globals.AttributesField) { + break + } + attrs := &pb.LdapAuthMethodAttributes{ + State: i.GetOperationalState(), + StartTls: i.GetStartTls(), + InsecureTls: i.GetInsecureTls(), + DiscoverDn: i.GetDiscoverDn(), + AnonGroupSearch: i.GetAnonGroupSearch(), + Urls: i.GetUrls(), + EnableGroups: i.GetEnableGroups(), + Certificates: i.GetCertificates(), + ClientCertificateKeyHmac: base64.RawURLEncoding.EncodeToString(i.GetClientCertificateKeyHmac()), + BindPasswordHmac: base64.RawURLEncoding.EncodeToString(i.GetBindPasswordHmac()), + UseTokenGroups: i.GetUseTokenGroups(), + } + if i.GetUpnDomain() != "" { + attrs.UpnDomain = wrapperspb.String(i.GetUpnDomain()) + } + if i.GetUserDn() != "" { + attrs.UserDn = wrapperspb.String(i.GetUserDn()) + } + if i.GetUserAttr() != "" { + attrs.UserAttr = wrapperspb.String(i.GetUserAttr()) + } + if i.GetUserFilter() != "" { + attrs.UserFilter = wrapperspb.String(i.GetUserFilter()) + } + if i.GetGroupDn() != "" { + attrs.GroupDn = wrapperspb.String(i.GetGroupDn()) + } + if i.GetGroupAttr() != "" { + attrs.GroupAttr = wrapperspb.String(i.GetGroupAttr()) + } + if i.GetGroupFilter() != "" { + attrs.GroupFilter = wrapperspb.String(i.GetGroupFilter()) + } + if i.GetClientCertificate() != "" { + attrs.ClientCertificate = wrapperspb.String(i.GetClientCertificate()) + } + if i.GetBindDn() != "" { + attrs.BindDn = wrapperspb.String(i.GetBindDn()) + } + if len(i.GetAccountAttributeMaps()) > 0 { + attrs.AccountAttributeMaps = i.GetAccountAttributeMaps() + } + + out.Attrs = &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: attrs, + } } return &out, nil } @@ -829,7 +966,7 @@ func validateGetRequest(req *pbs.GetAuthMethodRequest) error { if req == nil { return errors.NewDeprecated(errors.InvalidParameter, op, "Missing request") } - return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, globals.PasswordAuthMethodPrefix, globals.OidcAuthMethodPrefix) + return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, globals.PasswordAuthMethodPrefix, globals.OidcAuthMethodPrefix, globals.LdapAuthMethodPrefix) } func validateCreateRequest(ctx context.Context, req *pbs.CreateAuthMethodRequest) error { @@ -925,6 +1062,11 @@ func validateCreateRequest(ctx context.Context, req *pbs.CreateAuthMethodRequest } } } + case ldap.Subtype: + if len(req.GetItem().GetLdapAuthMethodsAttributes().GetUrls()) == 0 { + badFields[urlsField] = "At least one URL is required" + } + validateLdapAttributes(ctx, req.GetItem().GetLdapAuthMethodsAttributes(), badFields) default: badFields[typeField] = fmt.Sprintf("This is a required field and must be %q.", password.Subtype.String()) } @@ -1051,11 +1193,16 @@ func validateUpdateRequest(ctx context.Context, req *pbs.UpdateAuthMethodRequest } } } + case ldap.Subtype: + if req.GetItem().GetType() != "" && subtypes.SubtypeFromType(domain, req.GetItem().GetType()) != ldap.Subtype { + badFields[typeField] = "Cannot modify the resource type." + } + validateLdapAttributes(ctx, req.GetItem().GetLdapAuthMethodsAttributes(), badFields) default: badFields["id"] = "Incorrectly formatted identifier." } return badFields - }, globals.PasswordAuthMethodPrefix, globals.OidcAuthMethodPrefix) + }, globals.PasswordAuthMethodPrefix, globals.OidcAuthMethodPrefix, globals.LdapAuthMethodPrefix) } func validateDeleteRequest(req *pbs.DeleteAuthMethodRequest) error { @@ -1063,7 +1210,7 @@ func validateDeleteRequest(req *pbs.DeleteAuthMethodRequest) error { if req == nil { return errors.NewDeprecated(errors.InvalidParameter, op, "Missing request") } - return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, globals.PasswordAuthMethodPrefix, globals.OidcAuthMethodPrefix) + return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, globals.PasswordAuthMethodPrefix, globals.OidcAuthMethodPrefix, globals.LdapAuthMethodPrefix) } func validateListRequest(req *pbs.ListAuthMethodsRequest) error { @@ -1127,7 +1274,7 @@ func validateAuthenticateRequest(req *pbs.AuthenticateRequest) error { } else { st := subtypes.SubtypeFromId(domain, req.GetAuthMethodId()) switch st { - case password.Subtype, oidc.Subtype: + case password.Subtype, oidc.Subtype, ldap.Subtype: default: badFields[authMethodIdField] = "Unknown auth method type." } @@ -1267,6 +1414,14 @@ func transformAuthenticateRequestAttributes(msg proto.Message) error { default: return fmt.Errorf("%s: unknown command %q", op, authRequest.GetCommand()) } + case ldap.Subtype: + newAttrs := &pbs.LdapLoginAttributes{} + if err := handlers.StructToProto(attrs, newAttrs); err != nil { + return err + } + authRequest.Attrs = &pbs.AuthenticateRequest_LdapLoginAttributes{ + LdapLoginAttributes: newAttrs, + } default: return &subtypes.UnknownSubtypeIDError{ ID: authRequest.GetAuthMethodId(), diff --git a/internal/daemon/controller/handlers/authmethods/authmethod_service_test.go b/internal/daemon/controller/handlers/authmethods/authmethod_service_test.go index f9e5f570bc..8a92e93584 100644 --- a/internal/daemon/controller/handlers/authmethods/authmethod_service_test.go +++ b/internal/daemon/controller/handlers/authmethods/authmethod_service_test.go @@ -5,6 +5,10 @@ package authmethods_test import ( "context" + "crypto/ed25519" + "crypto/rand" + "crypto/x509" + "encoding/pem" "fmt" "regexp" "strings" @@ -12,6 +16,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/authtoken" @@ -27,7 +32,9 @@ import ( "github.com/hashicorp/boundary/internal/types/scope" pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/authmethods" scopepb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes" + "golang.org/x/exp/slices" "google.golang.org/grpc/codes" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" @@ -58,6 +65,13 @@ var ( action.ChangeState.String(), action.Authenticate.String(), } + ldapAuthorizedActions = []string{ + action.NoOp.String(), + action.Read.String(), + action.Update.String(), + action.Delete.String(), + action.Authenticate.String(), + } ) var authorizedCollectionActions = map[string]*structpb.ListValue{ @@ -87,6 +101,9 @@ func TestGet(t *testing.T) { oidcRepoFn := func() (*oidc.Repository, error) { return oidc.NewRepository(ctx, rw, rw, kmsCache) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kmsCache) + } pwRepoFn := func() (*password.Repository, error) { return password.NewRepository(rw, rw, kmsCache) } @@ -153,6 +170,32 @@ func TestGet(t *testing.T) { AuthorizedCollectionActions: authorizedCollectionActions, } + ldapAm := ldap.TestAuthMethod(t, conn, databaseWrapper, o.GetPublicId(), []string{"ldaps://ldap1"}, ldap.WithAccountAttributeMap(ctx, map[string]ldap.AccountToAttribute{ + "mail": ldap.ToEmailAttribute, + })) + wantLdap := &pb.AuthMethod{ + Id: ldapAm.GetPublicId(), + ScopeId: ldapAm.GetScopeId(), + CreatedTime: ldapAm.CreateTime.GetTimestamp(), + UpdatedTime: ldapAm.UpdateTime.GetTimestamp(), + Type: ldap.Subtype.String(), + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + State: string(ldap.InactiveState), + Urls: []string{"ldaps://ldap1"}, + AccountAttributeMaps: []string{"mail=email"}, + }, + }, + Version: 1, + Scope: &scopepb.ScopeInfo{ + Id: o.GetPublicId(), + Type: o.GetType(), + ParentScopeId: scope.Global.String(), + }, + AuthorizedActions: ldapAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + } + cases := []struct { name string scopeId string @@ -173,7 +216,13 @@ func TestGet(t *testing.T) { res: &pbs.GetAuthMethodResponse{Item: wantOidc}, }, { - name: "Get a non existant AuthMethod", + name: "Get an Existing LDAP AuthMethod", + scopeId: o.GetPublicId(), + req: &pbs.GetAuthMethodRequest{Id: ldapAm.GetPublicId()}, + res: &pbs.GetAuthMethodResponse{Item: wantLdap}, + }, + { + name: "Get a non existent AuthMethod", scopeId: o.GetPublicId(), req: &pbs.GetAuthMethodRequest{Id: globals.PasswordAuthMethodPrefix + "_DoesntExis"}, res: nil, @@ -198,7 +247,7 @@ func TestGet(t *testing.T) { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn) + s, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn) require.NoError(err, "Couldn't create new auth_method service.") got, gErr := s.GetAuthMethod(requestauth.DisabledAuthTestContext(iamRepoFn, tc.scopeId), tc.req) @@ -228,6 +277,9 @@ func TestList(t *testing.T) { oidcRepoFn := func() (*oidc.Repository, error) { return oidc.NewRepository(ctx, rw, rw, kmsCache) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kmsCache) + } pwRepoFn := func() (*password.Repository, error) { return password.NewRepository(rw, rw, kmsCache) } @@ -293,6 +345,42 @@ func TestList(t *testing.T) { }) } + sorterFn := func(a *pb.AuthMethod, b *pb.AuthMethod) bool { + switch { + case a.GetId() > b.GetId(): + return true + default: + return false + } + } + cpSorted := func(ams []*pb.AuthMethod) []*pb.AuthMethod { + cp := make([]*pb.AuthMethod, 0, len(ams)) + for _, a := range ams { + cp = append(cp, proto.Clone(a).(*pb.AuthMethod)) + } + slices.SortFunc(cp, sorterFn) + return cp + } + + ldapAm := ldap.TestAuthMethod(t, conn, databaseWrapper, oWithAuthMethods.GetPublicId(), []string{"ldaps://ldap1"}, ldap.WithOperationalState(ctx, ldap.ActivePublicState)) + wantSomeAuthMethods = append(wantSomeAuthMethods, &pb.AuthMethod{ + Id: ldapAm.GetPublicId(), + ScopeId: oWithAuthMethods.GetPublicId(), + CreatedTime: ldapAm.GetCreateTime().GetTimestamp(), + UpdatedTime: ldapAm.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: oWithAuthMethods.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, + Version: 1, + Type: ldap.Subtype.String(), + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + State: string(ldap.ActivePublicState), + Urls: []string{"ldaps://ldap1"}, + }, + }, + AuthorizedActions: ldapAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }) + var wantOtherAuthMethods []*pb.AuthMethod for _, aa := range password.TestAuthMethods(t, conn, oWithOtherAuthMethods.GetPublicId(), 3) { wantOtherAuthMethods = append(wantOtherAuthMethods, &pb.AuthMethod{ @@ -323,12 +411,12 @@ func TestList(t *testing.T) { { name: "List Some Auth Methods", req: &pbs.ListAuthMethodsRequest{ScopeId: oWithAuthMethods.GetPublicId()}, - res: &pbs.ListAuthMethodsResponse{Items: wantSomeAuthMethods}, + res: &pbs.ListAuthMethodsResponse{Items: cpSorted(wantSomeAuthMethods)}, }, { name: "List Other Auth Methods", req: &pbs.ListAuthMethodsRequest{ScopeId: oWithOtherAuthMethods.GetPublicId()}, - res: &pbs.ListAuthMethodsResponse{Items: wantOtherAuthMethods}, + res: &pbs.ListAuthMethodsResponse{Items: cpSorted(wantOtherAuthMethods)}, }, { name: "List No Auth Methods", @@ -344,7 +432,9 @@ func TestList(t *testing.T) { name: "List All Auth Methods Recursively", req: &pbs.ListAuthMethodsRequest{ScopeId: "global", Recursive: true}, res: &pbs.ListAuthMethodsResponse{ - Items: append(wantSomeAuthMethods, wantOtherAuthMethods...), + Items: func() []*pb.AuthMethod { + return cpSorted(append(wantSomeAuthMethods, wantOtherAuthMethods...)) + }(), }, }, { @@ -353,7 +443,7 @@ func TestList(t *testing.T) { ScopeId: "global", Recursive: true, Filter: fmt.Sprintf(`"/item/scope/id"==%q`, oWithAuthMethods.GetPublicId()), }, - res: &pbs.ListAuthMethodsResponse{Items: wantSomeAuthMethods}, + res: &pbs.ListAuthMethodsResponse{Items: cpSorted(wantSomeAuthMethods)}, }, { name: "Filter All Auth Methods", @@ -369,7 +459,7 @@ func TestList(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn) + s, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn) require.NoError(err, "Couldn't create new auth_method service.") // First check with non-anonymous user @@ -385,7 +475,11 @@ func TestList(t *testing.T) { assert.NotEqual("secret", oidcAttrs.ClientSecretHmac) } } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform(), protocmp.IgnoreFields(&pb.OidcAuthMethodAttributes{}, "client_secret_hmac")), + + slices.SortFunc(got.Items, sorterFn) + assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform(), + protocmp.IgnoreFields(&pb.OidcAuthMethodAttributes{}, "client_secret_hmac"), + protocmp.IgnoreFields(&pb.LdapAuthMethodAttributes{}, "bind_password_hmac", "client_certificate_key_hmac")), "ListAuthMethods() for scope %q got response %q, wanted %q", tc.req.GetScopeId(), got, tc.res) // Now check with anonymous user @@ -414,6 +508,9 @@ func TestDelete(t *testing.T) { oidcRepoFn := func() (*oidc.Repository, error) { return oidc.NewRepository(ctx, rw, rw, kmsCache) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kmsCache) + } pwRepoFn := func() (*password.Repository, error) { return password.NewRepository(rw, rw, kmsCache) } @@ -430,7 +527,9 @@ func TestDelete(t *testing.T) { oidcam := oidc.TestAuthMethod(t, conn, databaseWrapper, o.GetPublicId(), oidc.InactiveState, "alice_rp", "my-dogs-name", oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://alice.com")[0]), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://api.com")[0])) - s, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn) + ldapAm := ldap.TestAuthMethod(t, conn, databaseWrapper, o.GetPublicId(), []string{"ldaps://ldap1"}) + + s, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn) require.NoError(t, err, "Error when getting new auth_method service.") cases := []struct { @@ -453,6 +552,13 @@ func TestDelete(t *testing.T) { }, res: &pbs.DeleteAuthMethodResponse{}, }, + { + name: "Delete an Existing LDAP AuthMethod", + req: &pbs.DeleteAuthMethodRequest{ + Id: ldapAm.GetPublicId(), + }, + res: &pbs.DeleteAuthMethodResponse{}, + }, { name: "Delete bad auth_method id", req: &pbs.DeleteAuthMethodRequest{ @@ -494,6 +600,9 @@ func TestDelete_twice(t *testing.T) { oidcRepoFn := func() (*oidc.Repository, error) { return oidc.NewRepository(ctx, rw, rw, kms) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kms) + } pwRepoFn := func() (*password.Repository, error) { return password.NewRepository(rw, rw, kms) } @@ -505,7 +614,7 @@ func TestDelete_twice(t *testing.T) { o, _ := iam.TestScopes(t, iamRepo) am := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0] - s, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn) + s, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn) require.NoError(err, "Error when getting new auth_method service.") req := &pbs.DeleteAuthMethodRequest{ @@ -523,18 +632,21 @@ func TestCreate(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) wrapper := db.TestWrapper(t) - kms := kms.TestKms(t, conn, wrapper) + testKms := kms.TestKms(t, conn, wrapper) iamRepoFn := func() (*iam.Repository, error) { return iam.TestRepo(t, conn, wrapper), nil } pwRepoFn := func() (*password.Repository, error) { - return password.NewRepository(rw, rw, kms) + return password.NewRepository(rw, rw, testKms) } oidcRepoFn := func() (*oidc.Repository, error) { - return oidc.NewRepository(ctx, rw, rw, kms) + return oidc.NewRepository(ctx, rw, rw, testKms) + } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, testKms) } atRepoFn := func() (*authtoken.Repository, error) { - return authtoken.NewRepository(rw, rw, kms) + return authtoken.NewRepository(rw, rw, testKms) } iamRepo := iam.TestRepo(t, conn, wrapper) @@ -542,12 +654,20 @@ func TestCreate(t *testing.T) { defaultAm := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0] defaultCreated := defaultAm.GetCreateTime().GetTimestamp() + _, testEncodedCert := ldap.TestGenerateCA(t, "localhost") + _, privKey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + derEncodedKey, err := x509.MarshalPKCS8PrivateKey(privKey) + require.NoError(t, err) + testEncodedKey := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: derEncodedKey}) + cases := []struct { - name string - req *pbs.CreateAuthMethodRequest - res *pbs.CreateAuthMethodResponse - idPrefix string - err error + name string + req *pbs.CreateAuthMethodRequest + res *pbs.CreateAuthMethodResponse + idPrefix string + err error + errContains string }{ { name: "Create a valid Password AuthMethod", @@ -627,6 +747,74 @@ func TestCreate(t *testing.T) { }, }, }, + { + name: "create-a-valid-ldap-auth-method", + req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Type: ldap.Subtype.String(), + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + StartTls: true, + InsecureTls: true, + DiscoverDn: true, + AnonGroupSearch: true, + UpnDomain: wrapperspb.String("upn_domain"), + Urls: []string{"ldap://ldap1", "ldaps://ldap1"}, + BindDn: wrapperspb.String("bind-dn"), + BindPassword: wrapperspb.String("bind-password"), + UserDn: wrapperspb.String("user-dn"), + UserAttr: wrapperspb.String("user-attr"), + UserFilter: wrapperspb.String("user-filter"), + EnableGroups: true, + GroupDn: wrapperspb.String("group-dn"), + GroupAttr: wrapperspb.String("group-attr"), + GroupFilter: wrapperspb.String("group-filter"), + Certificates: []string{testEncodedCert}, + ClientCertificate: wrapperspb.String(testEncodedCert), + ClientCertificateKey: wrapperspb.String(string(testEncodedKey)), + UseTokenGroups: true, + AccountAttributeMaps: []string{"mail=email"}, + }, + }, + }}, + idPrefix: globals.LdapAuthMethodPrefix + "_", + res: &pbs.CreateAuthMethodResponse{ + Uri: fmt.Sprintf("auth-methods/%s_", globals.LdapAuthMethodPrefix), + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + CreatedTime: defaultAm.GetCreateTime().GetTimestamp(), + UpdatedTime: defaultAm.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()}, + Version: 1, + Type: ldap.Subtype.String(), + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + State: string(ldap.InactiveState), + StartTls: true, + InsecureTls: true, + DiscoverDn: true, + AnonGroupSearch: true, + UpnDomain: wrapperspb.String("upn_domain"), + Urls: []string{"ldap://ldap1", "ldaps://ldap1"}, + BindDn: wrapperspb.String("bind-dn"), + UserDn: wrapperspb.String("user-dn"), + UserAttr: wrapperspb.String("user-attr"), + UserFilter: wrapperspb.String("user-filter"), + EnableGroups: true, + GroupDn: wrapperspb.String("group-dn"), + GroupAttr: wrapperspb.String("group-attr"), + GroupFilter: wrapperspb.String("group-filter"), + Certificates: []string{testEncodedCert}, + ClientCertificate: wrapperspb.String(testEncodedCert), + UseTokenGroups: true, + AccountAttributeMaps: []string{"mail=email"}, + }, + }, + AuthorizedActions: ldapAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, { name: "Create a global Password AuthMethod", req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ @@ -698,6 +886,42 @@ func TestCreate(t *testing.T) { }, }, }, + { + name: "create-a-global-ldap-auth-method", + req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: scope.Global.String(), + Type: ldap.Subtype.String(), + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + Urls: []string{"ldap://ldap1", "ldaps://ldap1"}, + EnableGroups: true, + GroupDn: wrapperspb.String("group-dn"), + }, + }, + }}, + idPrefix: globals.LdapAuthMethodPrefix + "_", + res: &pbs.CreateAuthMethodResponse{ + Uri: fmt.Sprintf("auth-methods/%s_", globals.LdapAuthMethodPrefix), + Item: &pb.AuthMethod{ + ScopeId: scope.Global.String(), + CreatedTime: defaultAm.GetCreateTime().GetTimestamp(), + UpdatedTime: defaultAm.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: scope.Global.String(), Type: scope.Global.String(), Name: scope.Global.String(), Description: "Global Scope"}, + Version: 1, + Type: ldap.Subtype.String(), + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + State: string(ldap.InactiveState), + Urls: []string{"ldap://ldap1", "ldaps://ldap1"}, + EnableGroups: true, + GroupDn: wrapperspb.String("group-dn"), + }, + }, + AuthorizedActions: ldapAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, { name: "Can't specify Id", req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ @@ -955,18 +1179,186 @@ func TestCreate(t *testing.T) { }}, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, + { + name: "ldap-auth-method-requires-urls", + req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Type: ldap.Subtype.String(), + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{}, + }, + }}, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "At least one URL is required", + }, + { + name: "ldap-auth-method-invalid-urls", + req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Type: ldap.Subtype.String(), + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + Urls: []string{"ldap://ldap1", "not-ldap-scheme://ldap2"}, + }, + }, + }}, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "is not either ldap or ldaps", + }, + { + name: "ldap-auth-method-invalid-cert", + req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Type: ldap.Subtype.String(), + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + Urls: []string{"ldap://ldap1"}, + Certificates: []string{"invalid-cert"}, + }, + }, + }}, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "failed to parse certificate: invalid PEM encoding", + }, + { + name: "ldap-auth-method-missing-bind-dn", + req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Type: ldap.Subtype.String(), + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + Urls: []string{"ldap://ldap1"}, + BindPassword: wrapperspb.String("pass"), + }, + }, + }}, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "attributes.bind_password is missing required attributes.bind_dn field", + }, + { + name: "ldap-auth-method-missing-bind-password", + req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Type: ldap.Subtype.String(), + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + Urls: []string{"ldap://ldap1"}, + BindDn: wrapperspb.String("bind-dn"), + }, + }, + }}, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "attributes.bind_dn is missing required attributes.bind_password field", + }, + { + name: "ldap-auth-method-invalid-client-cert", + req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Type: ldap.Subtype.String(), + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + Urls: []string{"ldap://ldap1"}, + ClientCertificate: wrapperspb.String("invalid-cert"), + ClientCertificateKey: wrapperspb.String(string(testEncodedKey)), + }, + }, + }}, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "failed to parse certificate: invalid PEM encoding", + }, + { + name: "ldap-auth-method-invalid-client-cert-key", + req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Type: ldap.Subtype.String(), + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + Urls: []string{"ldap://ldap1"}, + ClientCertificate: wrapperspb.String(testEncodedCert), + ClientCertificateKey: wrapperspb.String("invalid-key"), + }, + }, + }}, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "attributes.client_certificate_key is not encoded as a valid pem", + }, + { + name: "ldap-auth-method-client-cert-key-not-a-key", + req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Type: ldap.Subtype.String(), + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + Urls: []string{"ldap://ldap1"}, + ClientCertificate: wrapperspb.String(testEncodedCert), + ClientCertificateKey: wrapperspb.String(testEncodedCert), + }, + }, + }}, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "attributes.client_certificate_key is not a valid private key", + }, + { + name: "ldap-auth-method-missing-client-cert-key", + req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Type: ldap.Subtype.String(), + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + Urls: []string{"ldap://ldap1"}, + ClientCertificate: wrapperspb.String(testEncodedCert), + }, + }, + }}, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "attributes.client_certificate is missing required attributes.client_certificate_key field", + }, + { + name: "ldap-auth-method-missing-client-cert", + req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Type: ldap.Subtype.String(), + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + Urls: []string{"ldap://ldap1"}, + ClientCertificateKey: wrapperspb.String(string(testEncodedKey)), + }, + }, + }}, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "attributes.client_certificate_key is missing required attributes.client_certificate field", + }, + { + name: "ldap-auth-method-invalid-attribute-map", + req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Type: ldap.Subtype.String(), + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + Urls: []string{"ldap://ldap1"}, + AccountAttributeMaps: []string{"invalid-map"}, + }, + }, + }}, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "invalid attributes.account_attribute_maps (unable to parse)", + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn) + s, err := authmethods.NewService(testKms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn) require.NoError(err, "Error when getting new auth_method service.") + conn.Debug(true) got, gErr := s.CreateAuthMethod(requestauth.DisabledAuthTestContext(iamRepoFn, tc.req.GetItem().GetScopeId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "CreateAuthMethod(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) + if tc.errContains != "" { + assert.Contains(gErr.Error(), tc.errContains) + } return } require.NoError(gErr) @@ -1003,6 +1395,14 @@ func TestCreate(t *testing.T) { protocmp.IgnoreFields(&pb.OidcAuthMethodAttributes{}, "client_secret_hmac", "callback_url"), ) } + if ldapAttrs := got.Item.GetLdapAuthMethodsAttributes(); ldapAttrs != nil { + assert.NotEqual(ldapAttrs.BindPassword, ldapAttrs.BindPasswordHmac) + cmpOptions = append( + cmpOptions, + protocmp.SortRepeatedFields(&pb.LdapAuthMethodAttributes{}, "account_attribute_maps", "urls", "certificates"), + protocmp.IgnoreFields(&pb.LdapAuthMethodAttributes{}, "bind_password_hmac", "client_certificate_key_hmac"), + ) + } } assert.Empty(cmp.Diff(got, tc.res, cmpOptions...), "CreateAuthMethod(%q) got response %q, wanted %q", tc.req, got, tc.res) }) diff --git a/internal/daemon/controller/handlers/authmethods/authmethod_test.go b/internal/daemon/controller/handlers/authmethods/authmethod_test.go index 1de49ff8ea..b5db0f33fc 100644 --- a/internal/daemon/controller/handlers/authmethods/authmethod_test.go +++ b/internal/daemon/controller/handlers/authmethods/authmethod_test.go @@ -51,6 +51,29 @@ func TestTransformAuthenticateRequestAttributes(t *testing.T) { }, }, }, + { + name: "ldap-attributes", + input: &pbs.AuthenticateRequest{ + AuthMethodId: "amldap_test", + Attrs: &pbs.AuthenticateRequest_Attributes{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "login_name": structpb.NewStringValue("login-name"), + "password": structpb.NewStringValue("password"), + }, + }, + }, + }, + expected: &pbs.AuthenticateRequest{ + AuthMethodId: "amldap_test", + Attrs: &pbs.AuthenticateRequest_LdapLoginAttributes{ + LdapLoginAttributes: &pbs.LdapLoginAttributes{ + LoginName: "login-name", + Password: "password", + }, + }, + }, + }, { name: "oidc-start-attributes", input: &pbs.AuthenticateRequest{ @@ -218,6 +241,20 @@ func TestTransformAuthenticateRequestAttributesErrors(t *testing.T) { }, })) }) + t.Run("invalid-ldap-attributes", func(t *testing.T) { + t.Parallel() + require.Error(t, transformAuthenticateRequestAttributes(&pbs.AuthenticateRequest{ + AuthMethodId: "amldap_test", + Attrs: &pbs.AuthenticateRequest_Attributes{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "field1": structpb.NewBoolValue(true), + "field2": structpb.NewStringValue("value2"), + }, + }, + }, + })) + }) t.Run("invalid-oidc-start-attributes", func(t *testing.T) { t.Parallel() require.Error(t, transformAuthenticateRequestAttributes(&pbs.AuthenticateRequest{ diff --git a/internal/daemon/controller/handlers/authmethods/ldap.go b/internal/daemon/controller/handlers/authmethods/ldap.go new file mode 100644 index 0000000000..59ef820925 --- /dev/null +++ b/internal/daemon/controller/handlers/authmethods/ldap.go @@ -0,0 +1,366 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package authmethods + +import ( + "context" + "crypto/x509" + "encoding/pem" + "fmt" + "net/url" + "strings" + + "github.com/hashicorp/boundary/internal/auth/ldap" + ldapstore "github.com/hashicorp/boundary/internal/auth/ldap/store" + "github.com/hashicorp/boundary/internal/daemon/controller/auth" + "github.com/hashicorp/boundary/internal/daemon/controller/handlers" + "github.com/hashicorp/boundary/internal/errors" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/hashicorp/boundary/internal/types/action" + pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/authmethods" + "google.golang.org/grpc/codes" +) + +var ldapMaskManager handlers.MaskManager + +func init() { + var err error + if ldapMaskManager, err = handlers.NewMaskManager(handlers.MaskDestination{&ldapstore.AuthMethod{}}, handlers.MaskSource{&pb.AuthMethod{}, &pb.LdapAuthMethodAttributes{}}); err != nil { + panic(err) + } + + IdActions[ldap.Subtype] = action.ActionSet{ + action.NoOp, + action.Read, + action.Update, + action.Delete, + action.Authenticate, + } +} + +const ( + urlsField = "attributes.urls" + bindDnField = "attributes.bind_dn" + bindPasswordField = "attributes.bind_password" + clientCertificateField = "attributes.client_certificate" + clientCertificateKeyField = "attributes.client_certificate_key" + certificatesField = "attributes.certificates" + accountAttributesMapField = "attributes.account_attribute_maps" +) + +func (s Service) authenticateLdap(ctx context.Context, req *pbs.AuthenticateRequest, authResults *auth.VerifyResults) (*pbs.AuthenticateResponse, error) { + const op = "authmethod_service.(Service).authenticateLdap" + if req == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "Nil request.") + } + if authResults == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "Nil auth results.") + } + reqAttrs := req.GetLdapLoginAttributes() + + ldapRepo, err := s.ldapRepoFn() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + ldapFn := func() (ldap.Authenticator, error) { + return ldapRepo, nil + } + + iamRepo, err := s.iamRepoFn() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + iamFn := func() (ldap.LookupUser, error) { + return iamRepo, nil + } + + atRepo, err := s.atRepoFn() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + atFn := func() (ldap.AuthTokenCreator, error) { + return atRepo, nil + } + + rawTk, err := ldap.Authenticate(ctx, ldapFn, iamFn, atFn, req.GetAuthMethodId(), reqAttrs.GetLoginName(), reqAttrs.GetPassword()) + if err != nil { + // let's not send back too much info about the error + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Unauthenticated, "Unable to authenticate.") + } + tk, err := s.ConvertInternalAuthTokenToApiAuthToken( + ctx, + rawTk, + ) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + return s.convertToAuthenticateResponse(ctx, req, authResults, tk) +} + +// createLdapInRepo creates an ldap auth method in a repo and returns the result. +// This method should never return a nil AuthMethod without returning an error. +func (s Service) createLdapInRepo(ctx context.Context, scopeId string, item *pb.AuthMethod) (*ldap.AuthMethod, error) { + u, err := toStorageLdapAuthMethod(ctx, scopeId, item) + if err != nil { + return nil, err + } + repo, err := s.ldapRepoFn() + if err != nil { + return nil, err + } + out, err := repo.CreateAuthMethod(ctx, u) + if err != nil { + return nil, fmt.Errorf("unable to create auth method: %w", err) + } + return out, nil +} + +func (s Service) updateLdapInRepo(ctx context.Context, scopeId, id string, mask []string, item *pb.AuthMethod) (*ldap.AuthMethod, error) { + u, err := toStorageLdapAuthMethod(ctx, scopeId, item) + if err != nil { + return nil, err + } + + version := item.GetVersion() + u.PublicId = id + + dbMask := ldapMaskManager.Translate(mask) + if len(dbMask) == 0 { + return nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."}) + } + + repo, err := s.ldapRepoFn() + if err != nil { + return nil, err + } + out, rowsUpdated, err := repo.UpdateAuthMethod(ctx, u, version, dbMask) + if err != nil { + return nil, fmt.Errorf("unable to update auth method: %w", err) + } + if rowsUpdated == 0 { + return nil, handlers.NotFoundErrorf("AuthMethod %q doesn't exist or incorrect version provided or no changes were made to the existing AuthMethod.", id) + } + return out, nil +} + +func toStorageLdapAuthMethod(ctx context.Context, scopeId string, in *pb.AuthMethod) (out *ldap.AuthMethod, err error) { + const op = "authmethod_service.toStorageLdapAuthMethod" + if in == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "nil auth method.") + } + attrs := in.GetLdapAuthMethodsAttributes() + + var opts []ldap.Option + if in.GetName() != nil { + opts = append(opts, ldap.WithName(ctx, in.GetName().GetValue())) + } + if in.GetDescription() != nil { + opts = append(opts, ldap.WithDescription(ctx, in.GetDescription().GetValue())) + } + var urls []*url.URL + if attrs != nil { + if attrs.GetState() != "" { + opts = append(opts, ldap.WithOperationalState(ctx, ldap.AuthMethodState(attrs.GetState()))) + } + if attrs.StartTls { + opts = append(opts, ldap.WithStartTLS(ctx)) + } + if attrs.InsecureTls { + opts = append(opts, ldap.WithInsecureTLS(ctx)) + } + if attrs.DiscoverDn { + opts = append(opts, ldap.WithDiscoverDn(ctx)) + } + if attrs.AnonGroupSearch { + opts = append(opts, ldap.WithAnonGroupSearch(ctx)) + } + if attrs.UpnDomain.GetValue() != "" { + opts = append(opts, ldap.WithUpnDomain(ctx, attrs.UpnDomain.GetValue())) + } + if attrs.UserDn.GetValue() != "" { + opts = append(opts, ldap.WithUserDn(ctx, attrs.UserDn.GetValue())) + } + if attrs.UserAttr.GetValue() != "" { + opts = append(opts, ldap.WithUserAttr(ctx, attrs.UserAttr.GetValue())) + } + if attrs.UserFilter.GetValue() != "" { + opts = append(opts, ldap.WithUserFilter(ctx, attrs.UserFilter.GetValue())) + } + if attrs.EnableGroups { + opts = append(opts, ldap.WithEnableGroups(ctx)) + } + if attrs.GroupDn.GetValue() != "" { + opts = append(opts, ldap.WithGroupDn(ctx, attrs.GroupDn.GetValue())) + } + if attrs.GroupAttr.GetValue() != "" { + opts = append(opts, ldap.WithGroupAttr(ctx, attrs.GroupAttr.GetValue())) + } + if attrs.GroupFilter.GetValue() != "" { + opts = append(opts, ldap.WithGroupFilter(ctx, attrs.GroupFilter.GetValue())) + } + if len(attrs.Certificates) > 0 { + certs, err := ldap.ParseCertificates(ctx, attrs.Certificates...) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + opts = append(opts, ldap.WithCertificates(ctx, certs...)) + } + if attrs.ClientCertificate != nil || attrs.ClientCertificateKey != nil { + keyBlk, _ := pem.Decode([]byte(attrs.ClientCertificateKey.GetValue())) + if keyBlk == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unable to parse %s PEM", clientCertificateKeyField)) + } + certBlk, _ := pem.Decode([]byte(attrs.ClientCertificate.GetValue())) + if certBlk == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unable to parse %s PEM", clientCertificateField)) + } + cc, err := x509.ParseCertificate(certBlk.Bytes) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to parse %s ASN.1 DER", clientCertificateField)) + } + opts = append(opts, ldap.WithClientCertificate(ctx, keyBlk.Bytes, cc)) + } + if attrs.BindDn.GetValue() != "" || attrs.BindPassword.GetValue() != "" { + opts = append(opts, ldap.WithBindCredential(ctx, attrs.BindDn.GetValue(), attrs.BindPassword.GetValue())) + } + if attrs.UseTokenGroups { + opts = append(opts, ldap.WithUseTokenGroups(ctx)) + } + if len(attrs.AccountAttributeMaps) > 0 { + attribMaps, err := ldap.ParseAccountAttributeMaps(ctx, attrs.AccountAttributeMaps...) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to parse %s", accountAttributesMapField)) + } + fromToMap := map[string]ldap.AccountToAttribute{} + for _, m := range attribMaps { + fromToMap[m.From] = ldap.AccountToAttribute(m.To) + } + opts = append(opts, ldap.WithAccountAttributeMap(ctx, fromToMap)) + } + + if len(attrs.GetUrls()) > 0 { + urls = make([]*url.URL, 0, len(attrs.GetUrls())) + for _, urlStr := range attrs.GetUrls() { + u, err := url.Parse(urlStr) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to parse %q into a url", urlStr)) + } + urls = append(urls, u) + } + opts = append(opts, ldap.WithUrls(ctx, urls...)) + } + } + u, err := ldap.NewAuthMethod(ctx, scopeId, opts...) + if err != nil { + switch { + case errors.Match(errors.T(errors.InvalidParameter), err): + return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, err.Error()) + default: + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build auth method: %v.", err) + } + } + return u, nil +} + +// validateLdapAttributes implements a handlers.CustomValidatorFunc(...) to be +// used when validating requests with ldap attributes. +func validateLdapAttributes(ctx context.Context, attrs *pb.LdapAuthMethodAttributes, badFields map[string]string) { + if attrs == nil { + // LDAP attributes are required when creating an LDAP auth method. + // badFields[attributesField] = "Attributes are required for creating an LDAP auth method." + return + } + if len(attrs.GetUrls()) > 0 { + badUrlMsgs := []string{} + for _, rawUrl := range attrs.GetUrls() { + u, err := url.Parse(rawUrl) + if err != nil { + badUrlMsgs = append(badUrlMsgs, fmt.Sprintf("%q is not a valid url", rawUrl)) + continue + } + if u.Scheme != "ldap" && u.Scheme != "ldaps" { + badUrlMsgs = append(badUrlMsgs, fmt.Sprintf("%s scheme in url %q is not either ldap or ldaps", u.Scheme, u.String())) + } + } + if len(badUrlMsgs) > 0 { + badFields[urlsField] = strings.Join(badUrlMsgs, " / ") + } + } + if len(attrs.GetCertificates()) > 0 { + if _, err := ldap.ParseCertificates(ctx, attrs.GetCertificates()...); err != nil { + badFields[certificatesField] = fmt.Sprintf("invalid %s: %s", certificatesField, err.Error()) + } + } + if attrs.GetBindDn().GetValue() != "" && attrs.GetBindPassword().GetValue() == "" { + badFields[bindPasswordField] = fmt.Sprintf("%s is missing required %s field", bindDnField, bindPasswordField) + } + if attrs.GetBindPassword().GetValue() != "" && attrs.GetBindDn().GetValue() == "" { + badFields[bindDnField] = fmt.Sprintf("%s is missing required %s field", bindPasswordField, bindDnField) + } + if attrs.GetClientCertificate().GetValue() != "" && attrs.GetClientCertificateKey().GetValue() == "" { + badFields[clientCertificateKeyField] = fmt.Sprintf("%s is missing required %s field", clientCertificateField, clientCertificateKeyField) + } + if attrs.GetClientCertificateKey().GetValue() != "" && attrs.GetClientCertificate().GetValue() == "" { + badFields[clientCertificateField] = fmt.Sprintf("%s is missing required %s field", clientCertificateKeyField, clientCertificateField) + } + if attrs.GetClientCertificate().GetValue() != "" { + if _, err := ldap.ParseCertificates(ctx, attrs.GetClientCertificate().GetValue()); err != nil { + badFields[clientCertificateField] = fmt.Sprintf("invalid %s: %s", clientCertificateField, err.Error()) + } + } + if attrs.GetClientCertificateKey().GetValue() != "" { + blk, _ := pem.Decode([]byte(attrs.GetClientCertificateKey().GetValue())) + if blk == nil || blk.Bytes == nil { + badFields[clientCertificateKeyField] = fmt.Sprintf("%s is not encoded as a valid pem", clientCertificateKeyField) + } else { + if _, err := x509.ParsePKCS8PrivateKey(blk.Bytes); err != nil { + badFields[clientCertificateKeyField] = fmt.Sprintf("%s is not a valid private key", clientCertificateKeyField) + } + } + } + if len(attrs.AccountAttributeMaps) > 0 { + if _, err := ldap.ParseAccountAttributeMaps(ctx, attrs.AccountAttributeMaps...); err != nil { + badFields[accountAttributesMapField] = fmt.Sprintf("invalid %s (unable to parse)", accountAttributesMapField) + } + } +} + +func validateAuthenticateLdapRequest(req *pbs.AuthenticateRequest) error { + badFields := make(map[string]string) + + attrs := req.GetLdapLoginAttributes() + switch { + case attrs == nil: + badFields["attributes"] = "This is a required field." + default: + if attrs.LoginName == "" { + badFields["attributes.login_name"] = "This is a required field." + } + if attrs.Password == "" { + badFields["attributes.password"] = "This is a required field." + } + if req.GetCommand() == "" { + // TODO: Eventually, require a command. For now, fall back to "login" for backwards compat. + req.Command = loginCommand + } + if req.Command != loginCommand { + badFields[commandField] = "Invalid command for this auth method type." + } + tokenType := req.GetType() + if tokenType == "" { + // Fall back to deprecated field if type is not set + tokenType = req.GetTokenType() //nolint:all + } + tType := strings.ToLower(strings.TrimSpace(tokenType)) + if tType != "" && tType != "token" && tType != "cookie" { + badFields[tokenTypeField] = `The only accepted types are "token" and "cookie".` + } + } + + if len(badFields) > 0 { + return handlers.InvalidArgumentErrorf("Invalid fields provided in request.", badFields) + } + return nil +} diff --git a/internal/daemon/controller/handlers/authmethods/ldap_test.go b/internal/daemon/controller/handlers/authmethods/ldap_test.go new file mode 100644 index 0000000000..77441f0a5b --- /dev/null +++ b/internal/daemon/controller/handlers/authmethods/ldap_test.go @@ -0,0 +1,983 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package authmethods_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/auth/ldap" + "github.com/hashicorp/boundary/internal/auth/oidc" + "github.com/hashicorp/boundary/internal/auth/password" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/daemon/controller/auth" + "github.com/hashicorp/boundary/internal/daemon/controller/handlers" + "github.com/hashicorp/boundary/internal/daemon/controller/handlers/authmethods" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/types/scope" + pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/authmethods" + scopepb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes" + "github.com/hashicorp/go-hclog" + "github.com/jimlambrt/gldap" + "github.com/jimlambrt/gldap/testdirectory" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/genproto/protobuf/field_mask" + "google.golang.org/grpc/codes" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/timestamppb" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +func Test_UpdateLdap(t *testing.T) { + t.Parallel() + ctx := context.TODO() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + iamRepoFn := func() (*iam.Repository, error) { + return iam.TestRepo(t, conn, wrapper), nil + } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(ctx, rw, rw, kms) + } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kms) + } + pwRepoFn := func() (*password.Repository, error) { + return password.NewRepository(rw, rw, kms) + } + atRepoFn := func() (*authtoken.Repository, error) { + return authtoken.NewRepository(rw, rw, kms) + } + iamRepo := iam.TestRepo(t, conn, wrapper) + + o, _ := iam.TestScopes(t, iamRepo) + tested, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn) + require.NoError(t, err, "Error when getting new auth_method service.") + + defaultScopeInfo := &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()} + + defaultAttributes := &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + Urls: []string{"ldaps://ldap1"}, + State: "active-private", + }, + } + defaultReadAttributes := &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + Urls: []string{"ldaps://ldap1"}, + State: "active-private", + }, + } + + freshAuthMethod := func(t *testing.T) (*pb.AuthMethod, func()) { + ctx := auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()) + am, err := tested.CreateAuthMethod(ctx, &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Name: wrapperspb.String("default"), + Description: wrapperspb.String("default"), + Type: ldap.Subtype.String(), + Attrs: defaultAttributes, + }}) + require.NoError(t, err) + + clean := func() { + _, err := tested.DeleteAuthMethod(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), + &pbs.DeleteAuthMethodRequest{Id: am.GetItem().GetId()}) + require.NoError(t, err) + } + return am.GetItem(), clean + } + + _, testEncodedCert := ldap.TestGenerateCA(t, "localhost") + + tests := []struct { + name string + req *pbs.UpdateAuthMethodRequest + res *pbs.UpdateAuthMethodResponse + err error + errContains string + wantErr bool + }{ + { + name: "update-an-existing-auth-method", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name", "description"}, + }, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: ldap.Subtype.String(), + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Version: 2, + Type: ldap.Subtype.String(), + Attrs: defaultReadAttributes, + Scope: defaultScopeInfo, + AuthorizedActions: ldapAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "multi-paths-in-single-string", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name,description,type"}, + }, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: ldap.Subtype.String(), + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Version: 2, + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: ldap.Subtype.String(), + Attrs: defaultReadAttributes, + Scope: defaultScopeInfo, + AuthorizedActions: ldapAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "no-update-mask", + req: &pbs.UpdateAuthMethodRequest{ + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "updated name"}, + Description: &wrapperspb.StringValue{Value: "updated desc"}, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "UpdateMask not provided but is required to update this resource", + }, + { + name: "missing-paths-in-mask", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{Paths: []string{}}, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "updated name"}, + Description: &wrapperspb.StringValue{Value: "updated desc"}, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "No valid fields included in the update mask", + }, + { + name: "non-existent-paths-in-mask", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{Paths: []string{"nonexistant_field"}}, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "updated name"}, + Description: &wrapperspb.StringValue{Value: "updated desc"}, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "No valid fields included in the update mask", + }, + { + name: "cannot-change-type", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{Paths: []string{"name", "type"}}, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "updated name"}, + Type: password.Subtype.String(), + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "Cannot modify the resource type", + }, + { + name: "cannot-change-is-primary", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{Paths: []string{"is_primary"}}, + Item: &pb.AuthMethod{ + IsPrimary: true, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "This field is read only", + }, + { + name: "unset-name", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name"}, + }, + Item: &pb.AuthMethod{ + Description: &wrapperspb.StringValue{Value: "ignored"}, + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Version: 2, + Description: &wrapperspb.StringValue{Value: "default"}, + Type: ldap.Subtype.String(), + Attrs: defaultReadAttributes, + Scope: defaultScopeInfo, + AuthorizedActions: ldapAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "unset-description", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"description"}, + }, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "ignored"}, + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Version: 2, + Name: &wrapperspb.StringValue{Value: "default"}, + Type: ldap.Subtype.String(), + Attrs: defaultReadAttributes, + Scope: defaultScopeInfo, + AuthorizedActions: ldapAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "update-only-state", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.state"}, + }, + Item: &pb.AuthMethod{ + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + State: "active-public", + }, + }, + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Version: 2, + Name: &wrapperspb.StringValue{Value: "default"}, + Description: &wrapperspb.StringValue{Value: "default"}, + Type: ldap.Subtype.String(), + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + Urls: []string{"ldaps://ldap1"}, + State: "active-public", + }, + }, + Scope: defaultScopeInfo, + AuthorizedActions: ldapAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "update-only-name", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name"}, + }, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "updated"}, + Description: &wrapperspb.StringValue{Value: "ignored"}, + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Version: 2, + Name: &wrapperspb.StringValue{Value: "updated"}, + Description: &wrapperspb.StringValue{Value: "default"}, + Type: ldap.Subtype.String(), + Attrs: defaultReadAttributes, + Scope: defaultScopeInfo, + AuthorizedActions: ldapAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "update-only-description", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"description"}, + }, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "ignored"}, + Description: &wrapperspb.StringValue{Value: "notignored"}, + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Version: 2, + Name: &wrapperspb.StringValue{Value: "default"}, + Description: &wrapperspb.StringValue{Value: "notignored"}, + Type: ldap.Subtype.String(), + Attrs: defaultReadAttributes, + Scope: defaultScopeInfo, + AuthorizedActions: ldapAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "update-a-non-existent-auth-method", + req: &pbs.UpdateAuthMethodRequest{ + Id: globals.LdapAuthMethodPrefix + "_DoesNotExist", + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"description"}, + }, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + }, + }, + err: handlers.ApiErrorWithCode(codes.NotFound), + errContains: "Resource not found", + }, + { + name: "cannot-change-id", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"id"}, + }, + Item: &pb.AuthMethod{ + Id: globals.LdapAuthMethodPrefix + "_something", + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "new desc"}, + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "This is a read only field and cannot be specified in an update request", + }, + { + name: "cannot-specify-created-time", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"created_time"}, + }, + Item: &pb.AuthMethod{ + CreatedTime: timestamppb.Now(), + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "This is a read only field and cannot be specified in an update request", + }, + { + name: "cannot-specify-updated-time", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"updated_time"}, + }, + Item: &pb.AuthMethod{ + UpdatedTime: timestamppb.Now(), + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "This is a read only field and cannot be specified in an update request", + }, + { + name: "cannot-specify-type", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"type"}, + }, + Item: &pb.AuthMethod{ + Type: ldap.Subtype.String(), + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "No valid fields included in the update mask", + }, + { + name: "cannot-change-scope", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"scope"}, + }, + Item: &pb.AuthMethod{ + ScopeId: "something-new", + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "new desc"}, + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "No valid fields included in the update mask", + }, + { + name: "invalid-certs", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.certificates"}, + }, + Item: &pb.AuthMethod{ + ScopeId: "something-new", + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + Certificates: []string{ldap.TestInvalidPem}, + }, + }, + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "invalid attributes.certificates", + }, + { + name: "update-certs", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.certificates"}, + }, + Item: &pb.AuthMethod{ + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + Certificates: []string{testEncodedCert}, + }, + }, + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Version: 2, + Name: &wrapperspb.StringValue{Value: "default"}, + Description: &wrapperspb.StringValue{Value: "default"}, + Type: ldap.Subtype.String(), + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + Urls: []string{"ldaps://ldap1"}, + State: "active-private", + Certificates: []string{testEncodedCert}, + }, + }, + Scope: defaultScopeInfo, + AuthorizedActions: ldapAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "update-urls", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.urls"}, + }, + Item: &pb.AuthMethod{ + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + Urls: []string{"ldaps://ldap2", "ldaps://ldap3"}, + }, + }, + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Version: 2, + Name: &wrapperspb.StringValue{Value: "default"}, + Description: &wrapperspb.StringValue{Value: "default"}, + Type: ldap.Subtype.String(), + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + Urls: []string{"ldaps://ldap2", "ldaps://ldap3"}, + State: "active-private", + }, + }, + Scope: defaultScopeInfo, + AuthorizedActions: ldapAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "update-urls-err", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.urls"}, + }, + Item: &pb.AuthMethod{ + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + Urls: []string{" ldaps://ldap2"}, // invalid url (space at start) + }, + }, + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "is not a valid url", + }, + { + name: "update-user-search-config", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.user_dn", "attributes.user_attr", "attributes.user_filter"}, + }, + Item: &pb.AuthMethod{ + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + UserDn: wrapperspb.String("user-dn"), + UserAttr: wrapperspb.String("user-attr"), + UserFilter: wrapperspb.String("user-filter"), + }, + }, + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Version: 2, + Name: &wrapperspb.StringValue{Value: "default"}, + Description: &wrapperspb.StringValue{Value: "default"}, + Type: ldap.Subtype.String(), + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + Urls: []string{"ldaps://ldap1"}, + State: "active-private", + UserDn: wrapperspb.String("user-dn"), + UserAttr: wrapperspb.String("user-attr"), + UserFilter: wrapperspb.String("user-filter"), + }, + }, + Scope: defaultScopeInfo, + AuthorizedActions: ldapAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "use-token-groups", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.use_token_groups"}, + }, + Item: &pb.AuthMethod{ + ScopeId: "something-new", + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + UseTokenGroups: true, + }, + }, + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Version: 2, + Name: &wrapperspb.StringValue{Value: "default"}, + Description: &wrapperspb.StringValue{Value: "default"}, + Type: ldap.Subtype.String(), + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + Urls: []string{"ldaps://ldap1"}, + State: "active-private", + UseTokenGroups: true, + }, + }, + Scope: defaultScopeInfo, + AuthorizedActions: ldapAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "enable-groups-err", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.enable_groups"}, + }, + Item: &pb.AuthMethod{ + ScopeId: "something-new", + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + EnableGroups: true, + }, + }, + }, + }, + res: nil, + wantErr: true, + errContains: "have a configured group_dn when enable_groups = true and use_token_groups = false", + }, + { + name: "update-group-search-config", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.group_dn", "attributes.group_attr", "attributes.group_filter"}, + }, + Item: &pb.AuthMethod{ + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + GroupDn: wrapperspb.String("group-dn"), + GroupAttr: wrapperspb.String("group-attr"), + GroupFilter: wrapperspb.String("group-filter"), + }, + }, + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Version: 2, + Name: &wrapperspb.StringValue{Value: "default"}, + Description: &wrapperspb.StringValue{Value: "default"}, + Type: ldap.Subtype.String(), + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + Urls: []string{"ldaps://ldap1"}, + State: "active-private", + GroupDn: wrapperspb.String("group-dn"), + GroupAttr: wrapperspb.String("group-attr"), + GroupFilter: wrapperspb.String("group-filter"), + }, + }, + Scope: defaultScopeInfo, + AuthorizedActions: ldapAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "no-change", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.certificates"}, + }, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "default"}, + }, + }, + err: handlers.ApiErrorWithCode(codes.NotFound), + errContains: "no changes were made to the existing AuthMethod", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + am, cleanup := freshAuthMethod(t) + defer cleanup() + + tc.req.Item.Version = am.GetVersion() + + if tc.req.GetId() == "" { + tc.req.Id = am.GetId() + } + + if tc.res != nil && tc.res.Item != nil { + tc.res.Item.Id = am.GetId() + tc.res.Item.CreatedTime = am.GetCreatedTime() + } + got, gErr := tested.UpdateAuthMethod(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req) + // TODO: When handlers move to domain errors remove wantErr and rely errors.Match here. + if tc.err != nil || tc.wantErr { + require.Error(gErr) + if tc.err != nil { + assert.True(errors.Is(gErr, tc.err), "UpdateAuthMethod(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) + } + if tc.errContains != "" { + assert.Contains(gErr.Error(), tc.errContains) + } + return + } + require.NoError(gErr) + if tc.res == nil { + require.Nil(got) + } + cmpOptions := []cmp.Option{protocmp.Transform()} + if got != nil { + assert.NotNilf(tc.res, "Expected UpdateAuthMethod response to be nil, but was %v", got) + gotUpdateTime := got.GetItem().GetUpdatedTime().AsTime() + created := am.GetCreatedTime().AsTime() + + // Verify it is a auth_method updated after it was created + assert.True(gotUpdateTime.After(created), "Updated auth_method should have been updated after it's creation. Was updated %v, which is after %v", gotUpdateTime, created) + + // Ignore all values which are hard to compare against. + cmpOptions = append( + cmpOptions, + protocmp.IgnoreFields(&pb.AuthMethod{}, "updated_time"), + protocmp.SortRepeatedFields(&pb.LdapAuthMethodAttributes{}, "account_attribute_maps", "urls", "certificates"), + protocmp.IgnoreFields(&pb.LdapAuthMethodAttributes{}, "bind_password_hmac", "client_certificate_key_hmac"), + ) + assert.NotEqual("bind_password", got.Item.GetLdapAuthMethodsAttributes().GetBindPasswordHmac()) + assert.NotEqual("client_certificate_key", got.Item.GetLdapAuthMethodsAttributes().GetClientCertificateKeyHmac()) + } + assert.Empty(cmp.Diff(got, tc.res, cmpOptions...), "UpdateAuthMethod(%q) got response %q, wanted %q", tc.req, got, tc.res) + }) + } +} + +func TestAuthenticate_Ldap(t *testing.T) { + t.Parallel() + testCtx := context.Background() + testConn, _ := db.TestSetup(t, "postgres") + testRw := db.New(testConn) + testRootWrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, testConn, testRootWrapper) + o, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testRootWrapper)) + + iamRepoFn := func() (*iam.Repository, error) { + return iam.TestRepo(t, testConn, testRootWrapper), nil + } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(testCtx, testRw, testRw, testKms) + } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(testCtx, testRw, testRw, testKms) + } + pwRepoFn := func() (*password.Repository, error) { + return password.NewRepository(testRw, testRw, testKms) + } + atRepoFn := func() (*authtoken.Repository, error) { + return authtoken.NewRepository(testRw, testRw, testKms) + } + + orgDbWrapper, err := testKms.GetWrapper(testCtx, o.GetPublicId(), kms.KeyPurposeDatabase) + require.NoError(t, err) + logger := hclog.New(&hclog.LoggerOptions{ + Name: "test-logger", + Level: hclog.Error, + }) + td := testdirectory.Start(t, + testdirectory.WithDefaults(t, &testdirectory.Defaults{AllowAnonymousBind: true}), + testdirectory.WithLogger(t, logger), + ) + groups := []*gldap.Entry{ + testdirectory.NewGroup(t, "admin", []string{"alice"}), + testdirectory.NewGroup(t, "users", []string{"alice"}), + } + users := testdirectory.NewUsers(t, []string{"alice"}, testdirectory.WithMembersOf(t, "admin", "users")) + users2 := testdirectory.NewUsers(t, []string{"bob"}) + td.SetUsers(append(users, users2...)...) + td.SetGroups(groups...) + + tdCerts, err := ldap.ParseCertificates(testCtx, td.Cert()) + require.NoError(t, err) + + testAm := ldap.TestAuthMethod(t, testConn, orgDbWrapper, o.PublicId, + []string{fmt.Sprintf("ldaps://127.0.0.1:%d", td.Port())}, + ldap.WithCertificates(testCtx, tdCerts...), + ldap.WithDiscoverDn(testCtx), + ldap.WithEnableGroups(testCtx), + ldap.WithUserDn(testCtx, testdirectory.DefaultUserDN), + ldap.WithGroupDn(testCtx, testdirectory.DefaultGroupDN), + ) + + iam.TestSetPrimaryAuthMethod(t, iam.TestRepo(t, testConn, testRootWrapper), o, testAm.PublicId) + + testManagedGrp := ldap.TestManagedGroup(t, testConn, testAm, []string{"cn=admin,ou=groups,dc=example,dc=org"}) + + const ( + testLoginName = "alice" + testPassword = "password" + testLoginName2 = "bob" + ) + + testAcct := ldap.TestAccount(t, testConn, testAm, testLoginName) + + tests := []struct { + name string + acctId string + request *pbs.AuthenticateRequest + wantType string + wantGroups []string + wantErr error + wantErrContains string + }{ + { + name: "basic-with-groups", + acctId: testAcct.PublicId, + request: &pbs.AuthenticateRequest{ + AuthMethodId: testAm.GetPublicId(), + TokenType: "token", + Attrs: &pbs.AuthenticateRequest_LdapLoginAttributes{ + LdapLoginAttributes: &pbs.LdapLoginAttributes{ + LoginName: testLoginName, + Password: testPassword, + }, + }, + }, + wantGroups: []string{testManagedGrp.PublicId}, + wantType: "token", + }, + { + name: "basic-without-groups", + request: &pbs.AuthenticateRequest{ + AuthMethodId: testAm.GetPublicId(), + TokenType: "token", + Attrs: &pbs.AuthenticateRequest_LdapLoginAttributes{ + LdapLoginAttributes: &pbs.LdapLoginAttributes{ + LoginName: testLoginName2, + Password: testPassword, + }, + }, + }, + wantType: "token", + }, + { + name: "cookie-type", + acctId: testAcct.PublicId, + request: &pbs.AuthenticateRequest{ + AuthMethodId: testAm.GetPublicId(), + TokenType: "cookie", + Attrs: &pbs.AuthenticateRequest_LdapLoginAttributes{ + LdapLoginAttributes: &pbs.LdapLoginAttributes{ + LoginName: testLoginName, + Password: testPassword, + }, + }, + }, + wantType: "cookie", + }, + { + name: "no-token-type", + acctId: testAcct.PublicId, + request: &pbs.AuthenticateRequest{ + AuthMethodId: testAm.GetPublicId(), + Attrs: &pbs.AuthenticateRequest_LdapLoginAttributes{ + LdapLoginAttributes: &pbs.LdapLoginAttributes{ + LoginName: testLoginName, + Password: testPassword, + }, + }, + }, + }, + { + name: "bad-token-type", + request: &pbs.AuthenticateRequest{ + AuthMethodId: testAm.GetPublicId(), + TokenType: "email", + Attrs: &pbs.AuthenticateRequest_LdapLoginAttributes{ + LdapLoginAttributes: &pbs.LdapLoginAttributes{ + LoginName: testLoginName, + Password: testPassword, + }, + }, + }, + wantErr: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "no-authmethod", + request: &pbs.AuthenticateRequest{ + Attrs: &pbs.AuthenticateRequest_LdapLoginAttributes{ + LdapLoginAttributes: &pbs.LdapLoginAttributes{ + LoginName: testLoginName, + Password: testPassword, + }, + }, + }, + wantErr: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "wrong-password", + request: &pbs.AuthenticateRequest{ + AuthMethodId: testAm.GetPublicId(), + TokenType: "token", + Attrs: &pbs.AuthenticateRequest_LdapLoginAttributes{ + LdapLoginAttributes: &pbs.LdapLoginAttributes{ + LoginName: testLoginName, + Password: "wrong", + }, + }, + }, + wantErr: handlers.ApiErrorWithCode(codes.Unauthenticated), + }, + { + name: "wrong-login-name", + request: &pbs.AuthenticateRequest{ + AuthMethodId: testAm.GetPublicId(), + TokenType: "token", + Attrs: &pbs.AuthenticateRequest_LdapLoginAttributes{ + LdapLoginAttributes: &pbs.LdapLoginAttributes{ + LoginName: "wrong", + Password: testPassword, + }, + }, + }, + wantErr: handlers.ApiErrorWithCode(codes.Unauthenticated), + }, + { + name: "no-attributes", + request: &pbs.AuthenticateRequest{ + AuthMethodId: testAm.GetPublicId(), + TokenType: "token", + Attrs: &pbs.AuthenticateRequest_LdapLoginAttributes{}, + }, + wantErr: handlers.ApiErrorWithCode(codes.InvalidArgument), + wantErrContains: `Details: {{name: "attributes", desc: "This is a required field."}}`, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + s, err := authmethods.NewService(testKms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn) + require.NoError(err) + + resp, err := s.Authenticate(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.request) + if tc.wantErr != nil { + assert.Error(err) + assert.Truef(errors.Is(err, tc.wantErr), "Got %#v, wanted %#v", err, tc.wantErr) + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + + aToken := resp.GetAuthTokenResponse() + assert.NotEmpty(aToken.GetId()) + assert.NotEmpty(aToken.GetToken()) + assert.True(strings.HasPrefix(aToken.GetToken(), aToken.GetId())) + assert.Equal(testAm.GetPublicId(), aToken.GetAuthMethodId()) + assert.Equal(aToken.GetCreatedTime(), aToken.GetUpdatedTime()) + assert.Equal(aToken.GetCreatedTime(), aToken.GetApproximateLastUsedTime()) + assert.Equal(testAm.GetPublicId(), aToken.GetAuthMethodId()) + assert.Equal(tc.wantType, resp.GetType()) + + // support testing for pre-provisioned accounts + if tc.acctId != "" { + assert.Equal(tc.acctId, aToken.GetAccountId()) + } + + names := ldap.TestGetAcctManagedGroups(t, testConn, aToken.GetAccountId()) + if len(tc.wantGroups) > 0 { + assert.Equal(tc.wantGroups, names) + } + }) + } +} diff --git a/internal/daemon/controller/handlers/authmethods/oidc_test.go b/internal/daemon/controller/handlers/authmethods/oidc_test.go index 592b0db2d2..8de3d0931a 100644 --- a/internal/daemon/controller/handlers/authmethods/oidc_test.go +++ b/internal/daemon/controller/handlers/authmethods/oidc_test.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/boundary/globals" authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" + "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/authtoken" @@ -58,6 +59,7 @@ type setup struct { iamRepo *iam.Repository iamRepoFn common.IamRepoFactory oidcRepoFn common.OidcAuthRepoFactory + ldapRepoFn common.LdapAuthRepoFactory pwRepoFn common.PasswordAuthRepoFactory atRepoFn common.AuthTokenRepoFactory org *iam.Scope @@ -92,6 +94,9 @@ func getSetup(t *testing.T) setup { ret.oidcRepoFn = func() (*oidc.Repository, error) { return oidc.NewRepository(ctx, ret.rw, ret.rw, ret.kmsCache) } + ret.ldapRepoFn = func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, ret.rw, ret.rw, ret.kmsCache) + } ret.pwRepoFn = func() (*password.Repository, error) { return password.NewRepository(ret.rw, ret.rw, ret.kmsCache) } @@ -103,7 +108,7 @@ func getSetup(t *testing.T) setup { ret.databaseWrapper, err = ret.kmsCache.GetWrapper(ret.ctx, ret.org.PublicId, kms.KeyPurposeDatabase) require.NoError(err) - ret.authMethodService, err = authmethods.NewService(ret.kmsCache, ret.pwRepoFn, ret.oidcRepoFn, ret.iamRepoFn, ret.atRepoFn) + ret.authMethodService, err = authmethods.NewService(ret.kmsCache, ret.pwRepoFn, ret.oidcRepoFn, ret.iamRepoFn, ret.atRepoFn, ret.ldapRepoFn) require.NoError(err) ret.testProvider = capoidc.StartTestProvider(t) @@ -149,6 +154,9 @@ func TestList_FilterNonPublic(t *testing.T) { oidcRepoFn := func() (*oidc.Repository, error) { return oidc.NewRepository(ctx, rw, rw, kmsCache) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kmsCache) + } pwRepoFn := func() (*password.Repository, error) { return password.NewRepository(rw, rw, kmsCache) } @@ -185,7 +193,7 @@ func TestList_FilterNonPublic(t *testing.T) { oidc.WithIssuer(oidc.TestConvertToUrls(t, fmt.Sprintf("https://alice%d.com", i))[0]), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://api.com")[0])) } - s, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn) + s, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn) require.NoError(t, err, "Couldn't create new auth_method service.") req := &pbs.ListAuthMethodsRequest{ @@ -251,6 +259,9 @@ func TestUpdate_OIDC(t *testing.T) { oidcRepoFn := func() (*oidc.Repository, error) { return oidc.NewRepository(ctx, rw, rw, kms) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kms) + } pwRepoFn := func() (*password.Repository, error) { return password.NewRepository(rw, rw, kms) } @@ -260,7 +271,7 @@ func TestUpdate_OIDC(t *testing.T) { iamRepo := iam.TestRepo(t, conn, wrapper) o, _ := iam.TestScopes(t, iamRepo) - tested, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn) + tested, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn) require.NoError(t, err, "Error when getting new auth_method service.") defaultScopeInfo := &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()} @@ -1059,6 +1070,9 @@ func TestUpdate_OIDCDryRun(t *testing.T) { oidcRepoFn := func() (*oidc.Repository, error) { return oidc.NewRepository(ctx, rw, rw, kmsCache) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kmsCache) + } pwRepoFn := func() (*password.Repository, error) { return password.NewRepository(rw, rw, kmsCache) } @@ -1118,7 +1132,7 @@ func TestUpdate_OIDCDryRun(t *testing.T) { }, } - tested, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn) + tested, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn) require.NoError(t, err, "Error when getting new auth_method service.") cases := []struct { name string @@ -1235,6 +1249,9 @@ func TestChangeState_OIDC(t *testing.T) { oidcRepoFn := func() (*oidc.Repository, error) { return oidc.NewRepository(ctx, rw, rw, kmsCache) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kmsCache) + } pwRepoFn := func() (*password.Repository, error) { return password.NewRepository(rw, rw, kmsCache) } @@ -1264,7 +1281,7 @@ func TestChangeState_OIDC(t *testing.T) { mismatchedAM := oidc.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, "inactive", "different_client_id", oidc.ClientSecret(tpClientSecret), oidc.WithIssuer(oidc.TestConvertToUrls(t, tp.Addr())[0]), oidc.WithSigningAlgs(oidc.EdDSA), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://example.callback:58")[0]), oidc.WithCertificates(tpCert...)) - s, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn) + s, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn) require.NoError(t, err, "Error when getting new auth_method service.") wantTemplate := &pb.AuthMethod{ diff --git a/internal/daemon/controller/handlers/authmethods/password_test.go b/internal/daemon/controller/handlers/authmethods/password_test.go index cf057d5e70..d8e975d4d9 100644 --- a/internal/daemon/controller/handlers/authmethods/password_test.go +++ b/internal/daemon/controller/handlers/authmethods/password_test.go @@ -10,6 +10,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/authtoken" @@ -45,6 +46,9 @@ func TestUpdate_Password(t *testing.T) { oidcRepoFn := func() (*oidc.Repository, error) { return oidc.NewRepository(ctx, rw, rw, kms) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kms) + } pwRepoFn := func() (*password.Repository, error) { return password.NewRepository(rw, rw, kms) } @@ -54,7 +58,7 @@ func TestUpdate_Password(t *testing.T) { iamRepo := iam.TestRepo(t, conn, wrapper) o, _ := iam.TestScopes(t, iamRepo) - tested, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn) + tested, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn) require.NoError(t, err, "Error when getting new auth_method service.") defaultScopeInfo := &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()} @@ -482,6 +486,9 @@ func TestAuthenticate_Password(t *testing.T) { oidcRepoFn := func() (*oidc.Repository, error) { return oidc.NewRepository(ctx, rw, rw, kms) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kms) + } pwRepoFn := func() (*password.Repository, error) { return password.NewRepository(rw, rw, kms) } @@ -617,7 +624,7 @@ func TestAuthenticate_Password(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn) + s, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn) require.NoError(err) resp, err := s.Authenticate(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.request) @@ -660,6 +667,9 @@ func TestAuthenticate_AuthAccountConnectedToIamUser_Password(t *testing.T) { oidcRepoFn := func() (*oidc.Repository, error) { return oidc.NewRepository(ctx, rw, rw, kms) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kms) + } pwRepoFn := func() (*password.Repository, error) { return password.NewRepository(rw, rw, kms) } @@ -683,7 +693,7 @@ func TestAuthenticate_AuthAccountConnectedToIamUser_Password(t *testing.T) { iamUser, err := iamRepo.LookupUserWithLogin(context.Background(), acct.GetPublicId()) require.NoError(err) - s, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn) + s, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn) require.NoError(err) resp, err := s.Authenticate(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), &pbs.AuthenticateRequest{ AuthMethodId: am.GetPublicId(), diff --git a/internal/daemon/controller/handlers/managed_groups/managed_group_service.go b/internal/daemon/controller/handlers/managed_groups/managed_group_service.go index 6a7dd649e7..e47f208243 100644 --- a/internal/daemon/controller/handlers/managed_groups/managed_group_service.go +++ b/internal/daemon/controller/handlers/managed_groups/managed_group_service.go @@ -5,10 +5,13 @@ package managed_groups import ( "context" + "encoding/json" "fmt" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth" + "github.com/hashicorp/boundary/internal/auth/ldap" + ldapstore "github.com/hashicorp/boundary/internal/auth/ldap/store" "github.com/hashicorp/boundary/internal/auth/oidc" oidcstore "github.com/hashicorp/boundary/internal/auth/oidc/store" requestauth "github.com/hashicorp/boundary/internal/daemon/controller/auth" @@ -29,13 +32,15 @@ import ( const ( // oidc field names - attrFilterField = "attributes.filter" + attrFilterField = "attributes.filter" + attrGroupNamesField = "attributes.group_names" domain = "auth" ) var ( oidcMaskManager handlers.MaskManager + ldapMaskManager handlers.MaskManager // IdActions contains the set of actions that can be performed on // individual resources @@ -46,6 +51,12 @@ var ( action.Update, action.Delete, }, + ldap.Subtype: { + action.NoOp, + action.Read, + action.Update, + action.Delete, + }, } // CollectionActions contains the set of actions that can be performed on @@ -61,6 +72,9 @@ func init() { if oidcMaskManager, err = handlers.NewMaskManager(handlers.MaskDestination{&oidcstore.ManagedGroup{}}, handlers.MaskSource{&pb.ManagedGroup{}, &pb.OidcManagedGroupAttributes{}}); err != nil { panic(err) } + if ldapMaskManager, err = handlers.NewMaskManager(handlers.MaskDestination{&ldapstore.ManagedGroup{}}, handlers.MaskSource{&pb.ManagedGroup{}, &pb.LdapManagedGroupAttributes{}}); err != nil { + panic(err) + } } // Service handles request as described by the pbs.ManagedGroupServiceServer interface. @@ -68,17 +82,21 @@ type Service struct { pbs.UnsafeManagedGroupServiceServer oidcRepoFn common.OidcAuthRepoFactory + ldapRepoFn common.LdapAuthRepoFactory } var _ pbs.ManagedGroupServiceServer = (*Service)(nil) // NewService returns a managed group service which handles managed group related requests to boundary. -func NewService(oidcRepo common.OidcAuthRepoFactory) (Service, error) { +func NewService(ctx context.Context, oidcRepo common.OidcAuthRepoFactory, ldapRepo common.LdapAuthRepoFactory) (Service, error) { const op = "managed_groups.NewService" - if oidcRepo == nil { - return Service{}, errors.NewDeprecated(errors.InvalidParameter, op, "missing oidc repository provided") + switch { + case oidcRepo == nil: + return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing oidc repository provided") + case ldapRepo == nil: + return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing ldap repository provided") } - return Service{oidcRepoFn: oidcRepo}, nil + return Service{oidcRepoFn: oidcRepo, ldapRepoFn: ldapRepo}, nil } // ListManagedGroups implements the interface pbs.ManagedGroupsServiceServer. @@ -307,6 +325,29 @@ func (s Service) getFromRepo(ctx context.Context, id string) (auth.ManagedGroup, } } out = mg + case ldap.Subtype: + repo, err := s.ldapRepoFn() + if err != nil { + return nil, nil, err + } + mg, err := repo.LookupManagedGroup(ctx, id) + if err != nil { + if errors.IsNotFoundError(err) { + return nil, nil, handlers.NotFoundErrorf("LDAP ManagedGroup %q doesn't exist.", id) + } + return nil, nil, err + } + ids, err := repo.ListManagedGroupMembershipsByGroup(ctx, mg.GetPublicId()) + if err != nil { + return nil, nil, err + } + if len(ids) > 0 { + memberIds = make([]string, len(ids)) + for i, v := range ids { + memberIds[i] = v.MemberId + } + } + out = mg default: return nil, nil, handlers.NotFoundErrorf("Unrecognized id.") } @@ -328,7 +369,7 @@ func (s Service) createOidcInRepo(ctx context.Context, am auth.AuthMethod, item attrs := item.GetOidcManagedGroupAttributes() mg, err := oidc.NewManagedGroup(ctx, am.GetPublicId(), attrs.GetFilter(), opts...) if err != nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build user for creation: %v.", err) + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build managed group for creation: %v.", err) } repo, err := s.oidcRepoFn() if err != nil { @@ -345,6 +386,38 @@ func (s Service) createOidcInRepo(ctx context.Context, am auth.AuthMethod, item return out, nil } +func (s Service) createLdapInRepo(ctx context.Context, am auth.AuthMethod, item *pb.ManagedGroup) (*ldap.ManagedGroup, error) { + const op = "managed_groups.(Service).createLdapInRepo" + if item == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing item") + } + var opts []ldap.Option + if item.GetName() != nil { + opts = append(opts, ldap.WithName(ctx, item.GetName().GetValue())) + } + if item.GetDescription() != nil { + opts = append(opts, ldap.WithDescription(ctx, item.GetDescription().GetValue())) + } + attrs := item.GetLdapManagedGroupAttributes() + mg, err := ldap.NewManagedGroup(ctx, am.GetPublicId(), attrs.GetGroupNames(), opts...) + if err != nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build managed group for creation: %v.", err) + } + repo, err := s.ldapRepoFn() + if err != nil { + return nil, err + } + + out, err := repo.CreateManagedGroup(ctx, am.GetScopeId(), mg) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create managed group")) + } + if out == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create managed group but no error returned from repository.") + } + return out, nil +} + func (s Service) createInRepo(ctx context.Context, am auth.AuthMethod, item *pb.ManagedGroup) (auth.ManagedGroup, error) { const op = "managed_groups.(Service).createInRepo" if item == nil { @@ -361,6 +434,15 @@ func (s Service) createInRepo(ctx context.Context, am auth.AuthMethod, item *pb. return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create managed group but no error returned from repository.") } out = am + case ldap.Subtype: + am, err := s.createLdapInRepo(ctx, am, item) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + if am == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create ldap managed group but no error returned from repository.") + } + out = am } return out, nil } @@ -401,6 +483,46 @@ func (s Service) updateOidcInRepo(ctx context.Context, scopeId, amId, id string, return out, nil } +func (s Service) updateLdapInRepo(ctx context.Context, scopeId, amId, id string, mask []string, item *pb.ManagedGroup) (*ldap.ManagedGroup, error) { + const op = "managed_groups.(Service).updateLdapInRepo" + if item == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "nil managed group.") + } + mg := ldap.AllocManagedGroup() + mg.PublicId = id + if item.GetName() != nil { + mg.Name = item.GetName().GetValue() + } + if item.GetDescription() != nil { + mg.Description = item.GetDescription().GetValue() + } + // Set this regardless; it'll only take effect if the masks contain the value + encodedGroupNames, err := json.Marshal(item.GetLdapManagedGroupAttributes().GetGroupNames()) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to encode group names")) + } + mg.GroupNames = string(encodedGroupNames) + + version := item.GetVersion() + + dbMask := ldapMaskManager.Translate(mask) + if len(dbMask) == 0 { + return nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."}) + } + repo, err := s.ldapRepoFn() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + out, rowsUpdated, err := repo.UpdateManagedGroup(ctx, scopeId, mg, version, dbMask) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update managed group")) + } + if rowsUpdated == 0 { + return nil, handlers.NotFoundErrorf("Managed Group %q doesn't exist or incorrect version provided.", id) + } + return out, nil +} + func (s Service) updateInRepo(ctx context.Context, scopeId, authMethodId string, req *pbs.UpdateManagedGroupRequest) (auth.ManagedGroup, error) { const op = "managed_groups.(Service).updateInRepo" var out auth.ManagedGroup @@ -414,6 +536,15 @@ func (s Service) updateInRepo(ctx context.Context, scopeId, authMethodId string, return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to update managed group but no error returned from repository.") } out = mg + case ldap.Subtype: + mg, err := s.updateLdapInRepo(ctx, scopeId, authMethodId, req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem()) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + if mg == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to update managed group but no error returned from repository.") + } + out = mg } return out, nil } @@ -429,6 +560,12 @@ func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool, return false, iErr } rows, err = repo.DeleteManagedGroup(ctx, scopeId, id) + case ldap.Subtype: + repo, iErr := s.ldapRepoFn() + if iErr != nil { + return false, iErr + } + rows, err = repo.DeleteManagedGroup(ctx, scopeId, id) } if err != nil { if errors.IsNotFoundError(err) { @@ -456,6 +593,18 @@ func (s Service) listFromRepo(ctx context.Context, authMethodId string) ([]auth. for _, a := range oidcl { outUl = append(outUl, a) } + case ldap.Subtype: + ldapRepo, err := s.ldapRepoFn() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + oidcl, err := ldapRepo.ListManagedGroups(ctx, authMethodId) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + for _, a := range oidcl { + outUl = append(outUl, a) + } } return outUl, nil } @@ -468,6 +617,11 @@ func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Ty res.Error = err return nil, res } + ldapRepo, err := s.ldapRepoFn() + if err != nil { + res.Error = err + return nil, res + } var parentId string opts := []requestauth.Option{requestauth.WithType(resource.ManagedGroup), requestauth.WithAction(a)} @@ -477,16 +631,27 @@ func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Ty default: switch subtypes.SubtypeFromId(domain, id) { case oidc.Subtype: - acct, err := oidcRepo.LookupManagedGroup(ctx, id) + grp, err := oidcRepo.LookupManagedGroup(ctx, id) if err != nil { res.Error = err return nil, res } - if acct == nil { + if grp == nil { res.Error = handlers.NotFoundError() return nil, res } - parentId = acct.GetAuthMethodId() + parentId = grp.GetAuthMethodId() + case ldap.Subtype: + grp, err := ldapRepo.LookupManagedGroup(ctx, id) + if err != nil { + res.Error = err + return nil, res + } + if grp == nil { + res.Error = handlers.NotFoundError() + return nil, res + } + parentId = grp.GetAuthMethodId() default: res.Error = errors.New(ctx, errors.InvalidPublicId, op, "unrecognized managed group subtype") return nil, res @@ -508,6 +673,18 @@ func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Ty } authMeth = am opts = append(opts, requestauth.WithScopeId(am.GetScopeId())) + case ldap.Subtype: + am, err := ldapRepo.LookupAuthMethod(ctx, parentId) + if err != nil { + res.Error = err + return nil, res + } + if am == nil { + res.Error = handlers.NotFoundError() + return nil, res + } + authMeth = am + opts = append(opts, requestauth.WithScopeId(am.GetScopeId())) default: res.Error = errors.New(ctx, errors.InvalidPublicId, op, "unrecognized auth method subtype") return nil, res @@ -568,6 +745,25 @@ func toProto(ctx context.Context, in auth.ManagedGroup, opt ...handlers.Option) out.Attrs = &pb.ManagedGroup_OidcManagedGroupAttributes{ OidcManagedGroupAttributes: attrs, } + case *ldap.ManagedGroup: + if outputFields.Has(globals.TypeField) { + out.Type = ldap.Subtype.String() + } + if !outputFields.Has(globals.AttributesField) { + break + } + + var grpNames []string + if err := json.Unmarshal([]byte(i.GetGroupNames()), &grpNames); err != nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "unable to unmarshal group names") + } + + attrs := &pb.LdapManagedGroupAttributes{ + GroupNames: grpNames, + } + out.Attrs = &pb.ManagedGroup_LdapManagedGroupAttributes{ + LdapManagedGroupAttributes: attrs, + } } return &out, nil } @@ -582,7 +778,7 @@ func validateGetRequest(req *pbs.GetManagedGroupRequest) error { if req == nil { return errors.NewDeprecated(errors.InvalidParameter, op, "nil request") } - return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, globals.OidcManagedGroupPrefix) + return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, globals.OidcManagedGroupPrefix, globals.LdapManagedGroupPrefix) } func validateCreateRequest(req *pbs.CreateManagedGroupRequest) error { @@ -612,6 +808,18 @@ func validateCreateRequest(req *pbs.CreateManagedGroupRequest) error { } } } + case ldap.Subtype: + if req.GetItem().GetType() != "" && req.GetItem().GetType() != ldap.Subtype.String() { + badFields[globals.TypeField] = "Doesn't match the parent resource's type." + } + attrs := req.GetItem().GetLdapManagedGroupAttributes() + if attrs == nil { + badFields[globals.AttributesField] = "Attribute fields is required." + } else { + if len(attrs.GroupNames) == 0 { + badFields[attrGroupNamesField] = "This field is required." + } + } default: badFields[globals.AuthMethodIdField] = "Unknown auth method type from ID." } @@ -646,11 +854,21 @@ func validateUpdateRequest(req *pbs.UpdateManagedGroupRequest) error { } } } + case ldap.Subtype: + if req.GetItem().GetType() != "" && req.GetItem().GetType() != ldap.Subtype.String() { + badFields[globals.TypeField] = "Cannot modify the resource type." + } + attrs := req.GetItem().GetLdapManagedGroupAttributes() + if handlers.MaskContains(req.GetUpdateMask().GetPaths(), attrGroupNamesField) { + if len(attrs.GroupNames) == 0 { + badFields[attrFilterField] = "Field cannot be empty." + } + } default: badFields[globals.IdField] = "Unrecognized resource type." } return badFields - }, globals.OidcManagedGroupPrefix) + }, globals.OidcManagedGroupPrefix, globals.LdapManagedGroupPrefix) } func validateDeleteRequest(req *pbs.DeleteManagedGroupRequest) error { @@ -658,7 +876,7 @@ func validateDeleteRequest(req *pbs.DeleteManagedGroupRequest) error { if req == nil { return errors.NewDeprecated(errors.InvalidParameter, op, "nil request") } - return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, globals.OidcManagedGroupPrefix) + return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, globals.OidcManagedGroupPrefix, globals.LdapManagedGroupPrefix) } func validateListRequest(req *pbs.ListManagedGroupsRequest) error { @@ -667,7 +885,7 @@ func validateListRequest(req *pbs.ListManagedGroupsRequest) error { return errors.NewDeprecated(errors.InvalidParameter, op, "nil request") } badFields := map[string]string{} - if !handlers.ValidId(handlers.Id(req.GetAuthMethodId()), globals.OidcAuthMethodPrefix) { + if !handlers.ValidId(handlers.Id(req.GetAuthMethodId()), globals.OidcAuthMethodPrefix, globals.LdapAuthMethodPrefix) { badFields[globals.AuthMethodIdField] = "Invalid formatted identifier." } if _, err := handlers.NewFilter(req.GetFilter()); err != nil { diff --git a/internal/daemon/controller/handlers/managed_groups/managed_group_service_test.go b/internal/daemon/controller/handlers/managed_groups/managed_group_service_test.go index c9146932e2..6906f3379f 100644 --- a/internal/daemon/controller/handlers/managed_groups/managed_group_service_test.go +++ b/internal/daemon/controller/handlers/managed_groups/managed_group_service_test.go @@ -14,6 +14,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/daemon/controller/auth" @@ -37,12 +38,20 @@ import ( "google.golang.org/protobuf/types/known/wrapperspb" ) -var oidcAuthorizedActions = []string{ - action.NoOp.String(), - action.Read.String(), - action.Update.String(), - action.Delete.String(), -} +var ( + oidcAuthorizedActions = []string{ + action.NoOp.String(), + action.Read.String(), + action.Update.String(), + action.Delete.String(), + } + ldapAuthorizedActions = []string{ + action.NoOp.String(), + action.Read.String(), + action.Update.String(), + action.Delete.String(), + } +) func TestNewService(t *testing.T) { ctx := context.TODO() @@ -53,26 +62,41 @@ func TestNewService(t *testing.T) { oidcRepoFn := func() (*oidc.Repository, error) { return oidc.NewRepository(ctx, rw, rw, kmsCache) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kmsCache) + } cases := []struct { - name string - oidcRepo common.OidcAuthRepoFactory - wantErr bool + name string + oidcRepo common.OidcAuthRepoFactory + ldapRepo common.LdapAuthRepoFactory + wantErr bool + wantErrContains string }{ { - name: "nil-oidc-repo", - wantErr: true, + name: "nil-oidc-repo", + ldapRepo: ldapRepoFn, + wantErr: true, + wantErrContains: "missing oidc repository", + }, + { + name: "missing-ldap-repo", + oidcRepo: oidcRepoFn, + wantErr: true, + wantErrContains: "missing ldap repository", }, { name: "success", oidcRepo: oidcRepoFn, + ldapRepo: ldapRepoFn, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - _, err := managed_groups.NewService(tc.oidcRepo) + _, err := managed_groups.NewService(ctx, tc.oidcRepo, tc.ldapRepo) if tc.wantErr { assert.Error(t, err) + assert.Contains(t, err.Error(), tc.wantErrContains) } else { assert.NoError(t, err) } @@ -92,8 +116,11 @@ func TestGet(t *testing.T) { iamRepoFn := func() (*iam.Repository, error) { return iam.NewRepository(rw, rw, kmsCache) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kmsCache) + } - s, err := managed_groups.NewService(oidcRepoFn) + s, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn) require.NoError(t, err, "Couldn't create new managed groups service.") org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) @@ -140,11 +167,32 @@ func TestGet(t *testing.T) { MemberIds: []string{oidcA.GetPublicId()}, } + ldapAm := ldap.TestAuthMethod(t, conn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}) + ldapAcct := ldap.TestAccount(t, conn, ldapAm, "test-login-name", ldap.WithMemberOfGroups(ctx, "admin")) + ldapMg := ldap.TestManagedGroup(t, conn, ldapAm, []string{"admin"}) + ldapWireManagedGroup := pb.ManagedGroup{ + Id: ldapMg.GetPublicId(), + AuthMethodId: ldapAm.GetPublicId(), + CreatedTime: ldapMg.GetCreateTime().GetTimestamp(), + UpdatedTime: ldapMg.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: org.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, + Version: ldapMg.GetVersion(), + Type: "ldap", + Attrs: &pb.ManagedGroup_LdapManagedGroupAttributes{ + LdapManagedGroupAttributes: &pb.LdapManagedGroupAttributes{ + GroupNames: []string{"admin"}, + }, + }, + AuthorizedActions: ldapAuthorizedActions, + MemberIds: []string{ldapAcct.GetPublicId()}, + } + cases := []struct { - name string - req *pbs.GetManagedGroupRequest - res *pbs.GetManagedGroupResponse - err error + name string + req *pbs.GetManagedGroupRequest + res *pbs.GetManagedGroupResponse + err error + errContains string }{ { name: "Get an oidc managed group", @@ -152,22 +200,44 @@ func TestGet(t *testing.T) { res: &pbs.GetManagedGroupResponse{Item: &oidcWireManagedGroup}, }, { - name: "Get a non existing oidc managed group", - req: &pbs.GetManagedGroupRequest{Id: globals.OidcManagedGroupPrefix + "_DoesntExis"}, - res: nil, - err: handlers.ApiErrorWithCode(codes.NotFound), + name: "Get a non existing oidc managed group", + req: &pbs.GetManagedGroupRequest{Id: globals.OidcManagedGroupPrefix + "_DoesntExis"}, + res: nil, + err: handlers.ApiErrorWithCode(codes.NotFound), + errContains: "Resource not found.", + }, + { + name: "Get an ldap managed group", + req: &pbs.GetManagedGroupRequest{Id: ldapWireManagedGroup.GetId()}, + res: &pbs.GetManagedGroupResponse{Item: &ldapWireManagedGroup}, + }, + { + name: "space in id", + req: &pbs.GetManagedGroupRequest{Id: globals.AuthTokenPrefix + "_1 23456789"}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "Invalid formatted identifier", + }, + { + name: "Get a non existing ldap managed group", + req: &pbs.GetManagedGroupRequest{Id: globals.LdapManagedGroupPrefix + "_DoesntExis"}, + res: nil, + err: handlers.ApiErrorWithCode(codes.NotFound), + errContains: "Resource not found.", }, { - name: "Wrong id prefix", - req: &pbs.GetManagedGroupRequest{Id: "j_1234567890"}, - res: nil, - err: handlers.ApiErrorWithCode(codes.InvalidArgument), + name: "Wrong id prefix", + req: &pbs.GetManagedGroupRequest{Id: "j_1234567890"}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "Invalid formatted identifier.", }, { - name: "space in id", - req: &pbs.GetManagedGroupRequest{Id: globals.AuthTokenPrefix + "_1 23456789"}, - res: nil, - err: handlers.ApiErrorWithCode(codes.InvalidArgument), + name: "space in id", + req: &pbs.GetManagedGroupRequest{Id: globals.AuthTokenPrefix + "_1 23456789"}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "Invalid formatted identifier.", }, } for _, tc := range cases { @@ -177,6 +247,8 @@ func TestGet(t *testing.T) { if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "GetManagedGroup(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) + require.NotEmpty(tc.errContains) + assert.Contains(gErr.Error(), tc.errContains) return } require.NoError(gErr) @@ -197,6 +269,9 @@ func TestListOidc(t *testing.T) { iamRepoFn := func() (*iam.Repository, error) { return iam.NewRepository(rw, rw, kmsCache) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kmsCache) + } o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) databaseWrapper, err := kmsCache.GetWrapper(ctx, o.PublicId, kms.KeyPurposeDatabase) @@ -303,13 +378,171 @@ func TestListOidc(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := managed_groups.NewService(oidcRepoFn) + s, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn) + require.NoError(err, "Couldn't create new managed group service.") + + got, gErr := s.ListManagedGroups(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req) + if tc.err != nil { + require.Error(gErr) + assert.True(errors.Is(gErr, tc.err), "ListManagedGroups() with auth method %q got error %v, wanted %v", tc.req, gErr, tc.err) + return + } else { + require.NoError(gErr) + } + sort.Slice(got.Items, func(i, j int) bool { + return strings.Compare(got.Items[i].GetName().GetValue(), + got.Items[j].GetName().GetValue()) < 0 + }) + assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListManagedGroups() with scope %q got response %q, wanted %q", tc.req, got, tc.res) + + // Now test with anon + if tc.skipAnon { + return + } + got, gErr = s.ListManagedGroups(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId(), auth.WithUserId(globals.AnonymousUserId)), tc.req) + require.NoError(gErr) + assert.Len(got.Items, len(tc.res.Items)) + for _, g := range got.GetItems() { + assert.Nil(g.Attrs) + assert.Nil(g.CreatedTime) + assert.Nil(g.UpdatedTime) + assert.Empty(g.Version) + } + }) + } +} + +func TestListLdap(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrap := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrap) + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(ctx, rw, rw, kmsCache) + } + iamRepoFn := func() (*iam.Repository, error) { + return iam.NewRepository(rw, rw, kmsCache) + } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kmsCache) + } + + o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) + databaseWrapper, err := kmsCache.GetWrapper(ctx, o.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + amNoManagedGroups := ldap.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, []string{"ldaps://no-managed-groups"}) + amSomeManagedGroups := ldap.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, []string{"ldaps://some-managed-groups"}) + amOtherManagedGroups := ldap.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, []string{"ldaps://other-managed-groups"}) + + testGroups := []string{"admin", "users"} + var wantSomeManagedGroups []*pb.ManagedGroup + for i := 0; i < 3; i++ { + mg := ldap.TestManagedGroup(t, conn, amSomeManagedGroups, testGroups, ldap.WithName(ctx, strconv.Itoa(i))) + wantSomeManagedGroups = append(wantSomeManagedGroups, &pb.ManagedGroup{ + Id: mg.GetPublicId(), + AuthMethodId: mg.GetAuthMethodId(), + Name: wrapperspb.String(strconv.Itoa(i)), + CreatedTime: mg.GetCreateTime().GetTimestamp(), + UpdatedTime: mg.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, + Version: 1, + Type: ldap.Subtype.String(), + Attrs: &pb.ManagedGroup_LdapManagedGroupAttributes{ + LdapManagedGroupAttributes: &pb.LdapManagedGroupAttributes{ + GroupNames: testGroups, + }, + }, + AuthorizedActions: ldapAuthorizedActions, + }) + } + + var wantOtherManagedGroups []*pb.ManagedGroup + for i := 0; i < 3; i++ { + mg := ldap.TestManagedGroup(t, conn, amOtherManagedGroups, testGroups, ldap.WithName(ctx, strconv.Itoa(i))) + wantOtherManagedGroups = append(wantOtherManagedGroups, &pb.ManagedGroup{ + Id: mg.GetPublicId(), + AuthMethodId: mg.GetAuthMethodId(), + Name: wrapperspb.String(strconv.Itoa(i)), + CreatedTime: mg.GetCreateTime().GetTimestamp(), + UpdatedTime: mg.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, + Version: 1, + Type: ldap.Subtype.String(), + Attrs: &pb.ManagedGroup_LdapManagedGroupAttributes{ + LdapManagedGroupAttributes: &pb.LdapManagedGroupAttributes{ + GroupNames: testGroups, + }, + }, + AuthorizedActions: ldapAuthorizedActions, + }) + } + + cases := []struct { + name string + req *pbs.ListManagedGroupsRequest + res *pbs.ListManagedGroupsResponse + err error + errContains string + skipAnon bool + }{ + { + name: "List Some ManagedGroups", + req: &pbs.ListManagedGroupsRequest{AuthMethodId: amSomeManagedGroups.GetPublicId()}, + res: &pbs.ListManagedGroupsResponse{Items: wantSomeManagedGroups}, + }, + { + name: "List Other ManagedGroups", + req: &pbs.ListManagedGroupsRequest{AuthMethodId: amOtherManagedGroups.GetPublicId()}, + res: &pbs.ListManagedGroupsResponse{Items: wantOtherManagedGroups}, + }, + { + name: "List No ManagedGroups", + req: &pbs.ListManagedGroupsRequest{AuthMethodId: amNoManagedGroups.GetPublicId()}, + res: &pbs.ListManagedGroupsResponse{}, + }, + { + name: "Unfound Auth Method", + req: &pbs.ListManagedGroupsRequest{AuthMethodId: globals.OidcAuthMethodPrefix + "_DoesntExis"}, + err: handlers.ApiErrorWithCode(codes.NotFound), + errContains: "Resource not found.", + }, + { + name: "Filter Some ManagedGroups", + req: &pbs.ListManagedGroupsRequest{ + AuthMethodId: amSomeManagedGroups.GetPublicId(), + Filter: fmt.Sprintf(`"/item/name"==%q`, wantSomeManagedGroups[1].Name.GetValue()), + }, + res: &pbs.ListManagedGroupsResponse{Items: wantSomeManagedGroups[1:2]}, + skipAnon: true, + }, + { + name: "Filter All ManagedGroups", + req: &pbs.ListManagedGroupsRequest{ + AuthMethodId: amSomeManagedGroups.GetPublicId(), + Filter: `"/item/id"=="noManagedGroupmatchesthis"`, + }, + res: &pbs.ListManagedGroupsResponse{}, + }, + { + name: "Filter Bad Format", + req: &pbs.ListManagedGroupsRequest{AuthMethodId: amSomeManagedGroups.GetPublicId(), Filter: `"//id/"=="bad"`}, + err: handlers.InvalidArgumentErrorf("bad format", nil), + errContains: "name: \"filter\", desc: \"This field could not be parsed.", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + s, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn) require.NoError(err, "Couldn't create new managed group service.") got, gErr := s.ListManagedGroups(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "ListManagedGroups() with auth method %q got error %v, wanted %v", tc.req, gErr, tc.err) + require.NotEmpty(tc.errContains) + assert.Contains(gErr.Error(), tc.errContains) return } else { require.NoError(gErr) @@ -349,6 +582,9 @@ func TestDelete(t *testing.T) { iamRepoFn := func() (*iam.Repository, error) { return iam.NewRepository(rw, rw, kmsCache) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kmsCache) + } o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) @@ -363,15 +599,19 @@ func TestDelete(t *testing.T) { ) oidcMg := oidc.TestManagedGroup(t, conn, oidcAm, oidc.TestFakeManagedGroupFilter) - s, err := managed_groups.NewService(oidcRepoFn) + ldapAm := ldap.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, []string{"ldaps://ldap1"}) + ldapMg := ldap.TestManagedGroup(t, conn, ldapAm, []string{"admin", "users"}) + + s, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn) require.NoError(t, err, "Error when getting new user service.") cases := []struct { - name string - scope string - req *pbs.DeleteManagedGroupRequest - res *pbs.DeleteManagedGroupResponse - err error + name string + scope string + req *pbs.DeleteManagedGroupRequest + res *pbs.DeleteManagedGroupResponse + err error + errContains string }{ { name: "Delete an existing oidc managed group", @@ -384,14 +624,30 @@ func TestDelete(t *testing.T) { req: &pbs.DeleteManagedGroupRequest{ Id: globals.OidcManagedGroupPrefix + "_doesntexis", }, - err: handlers.ApiErrorWithCode(codes.NotFound), + err: handlers.ApiErrorWithCode(codes.NotFound), + errContains: "Resource not found.", + }, + { + name: "Delete an existing ldap managed group", + req: &pbs.DeleteManagedGroupRequest{ + Id: ldapMg.GetPublicId(), + }, + }, + { + name: "Delete bad ldap managed group id", + req: &pbs.DeleteManagedGroupRequest{ + Id: globals.LdapManagedGroupPrefix + "_doesntexis", + }, + err: handlers.ApiErrorWithCode(codes.NotFound), + errContains: "Resource not found.", }, { name: "Bad managed group id formatting", req: &pbs.DeleteManagedGroupRequest{ Id: "bad_format", }, - err: handlers.ApiErrorWithCode(codes.InvalidArgument), + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "Incorrectly formatted identifier.", }, } for _, tc := range cases { @@ -401,6 +657,8 @@ func TestDelete(t *testing.T) { if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "DeleteManagedGroup(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) + require.NotEmpty(tc.errContains) + assert.Contains(gErr.Error(), tc.errContains) } assert.EqualValuesf(tc.res, got, "DeleteManagedGroup(%q) got response %q, wanted %q", tc.req, got, tc.res) }) @@ -421,6 +679,9 @@ func TestDelete_twice(t *testing.T) { iamRepoFn := func() (*iam.Repository, error) { return iam.NewRepository(rw, rw, kmsCache) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kmsCache) + } o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) @@ -436,7 +697,7 @@ func TestDelete_twice(t *testing.T) { ) oidcMg := oidc.TestManagedGroup(t, conn, oidcAm, oidc.TestFakeManagedGroupFilter) - s, err := managed_groups.NewService(oidcRepoFn) + s, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn) require.NoError(err, "Error when getting new user service") req := &pbs.DeleteManagedGroupRequest{ Id: oidcMg.GetPublicId(), @@ -460,8 +721,11 @@ func TestCreateOidc(t *testing.T) { iamRepoFn := func() (*iam.Repository, error) { return iam.NewRepository(rw, rw, kmsCache) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kmsCache) + } - s, err := managed_groups.NewService(oidcRepoFn) + s, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn) require.NoError(t, err, "Error when getting new managed group service.") o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) @@ -650,7 +914,7 @@ func TestCreateOidc(t *testing.T) { } } -func TestUpdateOidc(t *testing.T) { +func TestCreateLdap(t *testing.T) { ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) @@ -662,98 +926,307 @@ func TestUpdateOidc(t *testing.T) { iamRepoFn := func() (*iam.Repository, error) { return iam.NewRepository(rw, rw, kmsCache) } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kmsCache) + } - o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) + s, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn) + require.NoError(t, err, "Error when getting new managed group service.") + o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) databaseWrapper, err := kmsCache.GetWrapper(ctx, o.PublicId, kms.KeyPurposeDatabase) require.NoError(t, err) - am := oidc.TestAuthMethod( - t, conn, databaseWrapper, o.PublicId, oidc.ActivePrivateState, - "alice-rp", "fido", - oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.alice.com")[0]), - oidc.WithSigningAlgs(oidc.RS256), - oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0])) - - tested, err := managed_groups.NewService(oidcRepoFn) - require.NoError(t, err, "Error when getting new managed_groups service.") - - defaultScopeInfo := &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()} - defaultAttributes := &pb.ManagedGroup_OidcManagedGroupAttributes{ - OidcManagedGroupAttributes: &pb.OidcManagedGroupAttributes{ - Filter: oidc.TestFakeManagedGroupFilter, - }, - } - - modifiedAttributes := &pb.ManagedGroup_OidcManagedGroupAttributes{ - OidcManagedGroupAttributes: &pb.OidcManagedGroupAttributes{ - Filter: `"/token/zip" == "zap"`, - }, - } - - badAttributes := &pb.ManagedGroup_OidcManagedGroupAttributes{ - OidcManagedGroupAttributes: &pb.OidcManagedGroupAttributes{ - Filter: `"foobar"`, - }, - } - - freshManagedGroup := func(t *testing.T) (*oidc.ManagedGroup, func()) { - t.Helper() - mg := oidc.TestManagedGroup(t, conn, am, oidc.TestFakeManagedGroupFilter, oidc.WithName("default"), oidc.WithDescription("default")) - - clean := func() { - _, err := tested.DeleteManagedGroup(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), - &pbs.DeleteManagedGroupRequest{Id: mg.GetPublicId()}) - require.NoError(t, err) - } - - return mg, clean - } + am := ldap.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, []string{"ldaps://ldap1"}) cases := []struct { - name string - req *pbs.UpdateManagedGroupRequest - res *pbs.UpdateManagedGroupResponse - err error + name string + req *pbs.CreateManagedGroupRequest + res *pbs.CreateManagedGroupResponse + err error + errContains string }{ { - name: "Update an Existing AuthMethod", - req: &pbs.UpdateManagedGroupRequest{ - UpdateMask: &field_mask.FieldMask{ - Paths: []string{globals.NameField, globals.DescriptionField}, - }, + name: "Create a valid ManagedGroup", + req: &pbs.CreateManagedGroupRequest{ Item: &pb.ManagedGroup{ - Name: &wrapperspb.StringValue{Value: "new"}, - Description: &wrapperspb.StringValue{Value: "desc"}, - Type: oidc.Subtype.String(), + AuthMethodId: am.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "name"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: ldap.Subtype.String(), + Attrs: &pb.ManagedGroup_LdapManagedGroupAttributes{ + LdapManagedGroupAttributes: &pb.LdapManagedGroupAttributes{ + GroupNames: []string{"admin", "users"}, + }, + }, }, }, - res: &pbs.UpdateManagedGroupResponse{ + res: &pbs.CreateManagedGroupResponse{ + Uri: fmt.Sprintf("managed-groups/%s_", globals.LdapManagedGroupPrefix), Item: &pb.ManagedGroup{ - AuthMethodId: am.GetPublicId(), - Name: &wrapperspb.StringValue{Value: "new"}, - Description: &wrapperspb.StringValue{Value: "desc"}, - Type: oidc.Subtype.String(), - Attrs: defaultAttributes, - Scope: defaultScopeInfo, - AuthorizedActions: oidcAuthorizedActions, + AuthMethodId: am.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "name"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, + Version: 1, + Type: ldap.Subtype.String(), + Attrs: &pb.ManagedGroup_LdapManagedGroupAttributes{ + LdapManagedGroupAttributes: &pb.LdapManagedGroupAttributes{ + GroupNames: []string{"admin", "users"}, + }, + }, + AuthorizedActions: ldapAuthorizedActions, }, }, }, { - name: "Multiple Paths in single string", - req: &pbs.UpdateManagedGroupRequest{ - UpdateMask: &field_mask.FieldMask{ - Paths: []string{"name,description"}, - }, + name: "Create a valid ManagedGroup without type defined", + req: &pbs.CreateManagedGroupRequest{ Item: &pb.ManagedGroup{ - Name: &wrapperspb.StringValue{Value: "new"}, - Description: &wrapperspb.StringValue{Value: "desc"}, - Type: oidc.Subtype.String(), + AuthMethodId: am.GetPublicId(), + Attrs: &pb.ManagedGroup_LdapManagedGroupAttributes{ + LdapManagedGroupAttributes: &pb.LdapManagedGroupAttributes{ + GroupNames: []string{"admin", "users"}, + }, + }, }, }, - res: &pbs.UpdateManagedGroupResponse{ + res: &pbs.CreateManagedGroupResponse{ + Uri: fmt.Sprintf("managed-groups/%s_", globals.LdapManagedGroupPrefix), Item: &pb.ManagedGroup{ - AuthMethodId: am.GetPublicId(), + AuthMethodId: am.GetPublicId(), + Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, + Version: 1, + Type: ldap.Subtype.String(), + Attrs: &pb.ManagedGroup_LdapManagedGroupAttributes{ + LdapManagedGroupAttributes: &pb.LdapManagedGroupAttributes{ + GroupNames: []string{"admin", "users"}, + }, + }, + AuthorizedActions: oidcAuthorizedActions, + }, + }, + }, + { + name: "Cant specify mismatching type", + req: &pbs.CreateManagedGroupRequest{ + Item: &pb.ManagedGroup{ + AuthMethodId: am.GetPublicId(), + Type: password.Subtype.String(), + Attrs: &pb.ManagedGroup_LdapManagedGroupAttributes{ + LdapManagedGroupAttributes: &pb.LdapManagedGroupAttributes{ + GroupNames: []string{"admin", "users"}, + }, + }, + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "Doesn't match the parent resource's type.", + }, + { + name: "Can't specify Id", + req: &pbs.CreateManagedGroupRequest{ + Item: &pb.ManagedGroup{ + AuthMethodId: am.GetPublicId(), + Id: globals.LdapManagedGroupPrefix + "_notallowed", + Type: oidc.Subtype.String(), + Attrs: &pb.ManagedGroup_LdapManagedGroupAttributes{ + LdapManagedGroupAttributes: &pb.LdapManagedGroupAttributes{ + GroupNames: []string{"admin", "users"}, + }, + }, + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "name: \"id\", desc: \"This is a read only field.", + }, + { + name: "Can't specify Created Time", + req: &pbs.CreateManagedGroupRequest{ + Item: &pb.ManagedGroup{ + AuthMethodId: am.GetPublicId(), + CreatedTime: timestamppb.Now(), + Type: ldap.Subtype.String(), + Attrs: &pb.ManagedGroup_LdapManagedGroupAttributes{ + LdapManagedGroupAttributes: &pb.LdapManagedGroupAttributes{ + GroupNames: []string{"admin", "users"}, + }, + }, + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "name: \"created_time\", desc: \"This is a read only field.", + }, + { + name: "Can't specify Update Time", + req: &pbs.CreateManagedGroupRequest{ + Item: &pb.ManagedGroup{ + AuthMethodId: am.GetPublicId(), + UpdatedTime: timestamppb.Now(), + Type: ldap.Subtype.String(), + Attrs: &pb.ManagedGroup_LdapManagedGroupAttributes{ + LdapManagedGroupAttributes: &pb.LdapManagedGroupAttributes{ + GroupNames: []string{"admin", "users"}, + }, + }, + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "name: \"updated_time\", desc: \"This is a read only field.", + }, + { + name: "Can't specify group names", + req: &pbs.CreateManagedGroupRequest{ + Item: &pb.ManagedGroup{ + AuthMethodId: am.GetPublicId(), + Type: oidc.Subtype.String(), + Attrs: &pb.ManagedGroup_LdapManagedGroupAttributes{ + LdapManagedGroupAttributes: &pb.LdapManagedGroupAttributes{ + GroupNames: []string{}, + }, + }, + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "name: \"attributes.group_names\", desc: \"This field is required.", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, gErr := s.CreateManagedGroup(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req) + if tc.err != nil { + require.Error(gErr) + assert.True(errors.Is(gErr, tc.err), "CreateManagedGroup(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) + require.NotEmpty(tc.errContains) + assert.Contains(gErr.Error(), tc.errContains) + return + } + require.NoError(gErr) + if got != nil { + assert.Contains(got.GetUri(), tc.res.Uri) + assert.True(strings.HasPrefix(got.GetItem().GetId(), globals.LdapManagedGroupPrefix+"_")) + // Clear all values which are hard to compare against. + got.Uri, tc.res.Uri = "", "" + got.Item.Id, tc.res.Item.Id = "", "" + got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil + } + assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "CreateManagedGroup(%q) got response %q, wanted %q", tc.req, got, tc.res) + }) + } +} + +func TestUpdateOidc(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrap := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrap) + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(ctx, rw, rw, kmsCache) + } + iamRepoFn := func() (*iam.Repository, error) { + return iam.NewRepository(rw, rw, kmsCache) + } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kmsCache) + } + + o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) + + databaseWrapper, err := kmsCache.GetWrapper(ctx, o.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + am := oidc.TestAuthMethod( + t, conn, databaseWrapper, o.PublicId, oidc.ActivePrivateState, + "alice-rp", "fido", + oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.alice.com")[0]), + oidc.WithSigningAlgs(oidc.RS256), + oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0])) + + tested, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn) + require.NoError(t, err, "Error when getting new managed_groups service.") + + defaultScopeInfo := &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()} + defaultAttributes := &pb.ManagedGroup_OidcManagedGroupAttributes{ + OidcManagedGroupAttributes: &pb.OidcManagedGroupAttributes{ + Filter: oidc.TestFakeManagedGroupFilter, + }, + } + + modifiedAttributes := &pb.ManagedGroup_OidcManagedGroupAttributes{ + OidcManagedGroupAttributes: &pb.OidcManagedGroupAttributes{ + Filter: `"/token/zip" == "zap"`, + }, + } + + badAttributes := &pb.ManagedGroup_OidcManagedGroupAttributes{ + OidcManagedGroupAttributes: &pb.OidcManagedGroupAttributes{ + Filter: `"foobar"`, + }, + } + + freshManagedGroup := func(t *testing.T) (*oidc.ManagedGroup, func()) { + t.Helper() + mg := oidc.TestManagedGroup(t, conn, am, oidc.TestFakeManagedGroupFilter, oidc.WithName("default"), oidc.WithDescription("default")) + + clean := func() { + _, err := tested.DeleteManagedGroup(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), + &pbs.DeleteManagedGroupRequest{Id: mg.GetPublicId()}) + require.NoError(t, err) + } + + return mg, clean + } + + cases := []struct { + name string + req *pbs.UpdateManagedGroupRequest + res *pbs.UpdateManagedGroupResponse + err error + }{ + { + name: "Update an Existing AuthMethod", + req: &pbs.UpdateManagedGroupRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{globals.NameField, globals.DescriptionField}, + }, + Item: &pb.ManagedGroup{ + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: oidc.Subtype.String(), + }, + }, + res: &pbs.UpdateManagedGroupResponse{ + Item: &pb.ManagedGroup{ + AuthMethodId: am.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: oidc.Subtype.String(), + Attrs: defaultAttributes, + Scope: defaultScopeInfo, + AuthorizedActions: oidcAuthorizedActions, + }, + }, + }, + { + name: "Multiple Paths in single string", + req: &pbs.UpdateManagedGroupRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name,description"}, + }, + Item: &pb.ManagedGroup{ + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: oidc.Subtype.String(), + }, + }, + res: &pbs.UpdateManagedGroupResponse{ + Item: &pb.ManagedGroup{ + AuthMethodId: am.GetPublicId(), Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, Type: oidc.Subtype.String(), @@ -1034,3 +1507,397 @@ func TestUpdateOidc(t *testing.T) { }) } } + +func TestUpdateLdap(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrap := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrap) + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(ctx, rw, rw, kmsCache) + } + iamRepoFn := func() (*iam.Repository, error) { + return iam.NewRepository(rw, rw, kmsCache) + } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kmsCache) + } + + o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) + + databaseWrapper, err := kmsCache.GetWrapper(ctx, o.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + am := ldap.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, []string{"ldaps://ldap1"}) + + tested, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn) + require.NoError(t, err, "Error when getting new managed_groups service.") + + testGroups := []string{"test", "admin"} + testGroupsUpdated := []string{"users"} + + defaultScopeInfo := &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()} + defaultAttributes := &pb.ManagedGroup_LdapManagedGroupAttributes{ + LdapManagedGroupAttributes: &pb.LdapManagedGroupAttributes{ + GroupNames: testGroups, + }, + } + + modifiedAttributes := &pb.ManagedGroup_LdapManagedGroupAttributes{ + LdapManagedGroupAttributes: &pb.LdapManagedGroupAttributes{ + GroupNames: testGroupsUpdated, + }, + } + + badAttributes := &pb.ManagedGroup_LdapManagedGroupAttributes{ + LdapManagedGroupAttributes: &pb.LdapManagedGroupAttributes{}, + } + + freshManagedGroup := func(t *testing.T) (*ldap.ManagedGroup, func()) { + t.Helper() + mg := ldap.TestManagedGroup(t, conn, am, testGroups, ldap.WithName(ctx, "default"), ldap.WithDescription(ctx, "default")) + + clean := func() { + _, err := tested.DeleteManagedGroup(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), + &pbs.DeleteManagedGroupRequest{Id: mg.GetPublicId()}) + require.NoError(t, err) + } + + return mg, clean + } + + cases := []struct { + name string + req *pbs.UpdateManagedGroupRequest + res *pbs.UpdateManagedGroupResponse + err error + errContains string + }{ + { + name: "Update an Existing Group", + req: &pbs.UpdateManagedGroupRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{globals.NameField, globals.DescriptionField}, + }, + Item: &pb.ManagedGroup{ + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: ldap.Subtype.String(), + }, + }, + res: &pbs.UpdateManagedGroupResponse{ + Item: &pb.ManagedGroup{ + AuthMethodId: am.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: ldap.Subtype.String(), + Attrs: defaultAttributes, + Scope: defaultScopeInfo, + AuthorizedActions: ldapAuthorizedActions, + }, + }, + }, + { + name: "Multiple Paths in single string", + req: &pbs.UpdateManagedGroupRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name,description"}, + }, + Item: &pb.ManagedGroup{ + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: ldap.Subtype.String(), + }, + }, + res: &pbs.UpdateManagedGroupResponse{ + Item: &pb.ManagedGroup{ + AuthMethodId: am.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: ldap.Subtype.String(), + Attrs: defaultAttributes, + Scope: defaultScopeInfo, + AuthorizedActions: ldapAuthorizedActions, + }, + }, + }, + { + name: "No Update Mask", + req: &pbs.UpdateManagedGroupRequest{ + Item: &pb.ManagedGroup{ + Name: &wrapperspb.StringValue{Value: "updated name"}, + Description: &wrapperspb.StringValue{Value: "updated desc"}, + Attrs: modifiedAttributes, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "UpdateMask not provided but is required to update this resource.", + }, + { + name: "Cant change type", + req: &pbs.UpdateManagedGroupRequest{ + UpdateMask: &field_mask.FieldMask{Paths: []string{"name", "type"}}, + Item: &pb.ManagedGroup{ + Type: "oidc", + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "Cannot modify the resource type.", + }, + { + name: "No Paths in Mask", + req: &pbs.UpdateManagedGroupRequest{ + UpdateMask: &field_mask.FieldMask{Paths: []string{}}, + Item: &pb.ManagedGroup{ + Name: &wrapperspb.StringValue{Value: "updated name"}, + Description: &wrapperspb.StringValue{Value: "updated desc"}, + Attrs: modifiedAttributes, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "No valid fields included in the update mask.", + }, + { + name: "Only non-existent paths in Mask", + req: &pbs.UpdateManagedGroupRequest{ + UpdateMask: &field_mask.FieldMask{Paths: []string{"nonexistant_field"}}, + Item: &pb.ManagedGroup{ + Name: &wrapperspb.StringValue{Value: "updated name"}, + Description: &wrapperspb.StringValue{Value: "updated desc"}, + Attrs: modifiedAttributes, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "No valid fields included in the update mask.", + }, + { + name: "Unset Name", + req: &pbs.UpdateManagedGroupRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{globals.NameField}, + }, + Item: &pb.ManagedGroup{ + Description: &wrapperspb.StringValue{Value: "ignored"}, + Attrs: modifiedAttributes, + }, + }, + res: &pbs.UpdateManagedGroupResponse{ + Item: &pb.ManagedGroup{ + AuthMethodId: am.GetPublicId(), + Description: &wrapperspb.StringValue{Value: "default"}, + Type: ldap.Subtype.String(), + Attrs: defaultAttributes, + Scope: defaultScopeInfo, + AuthorizedActions: ldapAuthorizedActions, + }, + }, + }, + { + name: "Update Only Name", + req: &pbs.UpdateManagedGroupRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{globals.NameField}, + }, + Item: &pb.ManagedGroup{ + Name: &wrapperspb.StringValue{Value: "updated"}, + Description: &wrapperspb.StringValue{Value: "ignored"}, + Attrs: modifiedAttributes, + }, + }, + res: &pbs.UpdateManagedGroupResponse{ + Item: &pb.ManagedGroup{ + AuthMethodId: am.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "updated"}, + Description: &wrapperspb.StringValue{Value: "default"}, + Type: ldap.Subtype.String(), + Attrs: defaultAttributes, + Scope: defaultScopeInfo, + AuthorizedActions: ldapAuthorizedActions, + }, + }, + }, + { + name: "Update Only Description", + req: &pbs.UpdateManagedGroupRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{globals.DescriptionField}, + }, + Item: &pb.ManagedGroup{ + Name: &wrapperspb.StringValue{Value: "ignored"}, + Description: &wrapperspb.StringValue{Value: "notignored"}, + Attrs: modifiedAttributes, + }, + }, + res: &pbs.UpdateManagedGroupResponse{ + Item: &pb.ManagedGroup{ + AuthMethodId: am.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "default"}, + Description: &wrapperspb.StringValue{Value: "notignored"}, + Type: ldap.Subtype.String(), + Attrs: defaultAttributes, + Scope: defaultScopeInfo, + AuthorizedActions: ldapAuthorizedActions, + }, + }, + }, + { + name: "Update a Non Existing ManagedGroup", + req: &pbs.UpdateManagedGroupRequest{ + Id: globals.LdapManagedGroupPrefix + "_DoesntExis", + UpdateMask: &field_mask.FieldMask{ + Paths: []string{globals.DescriptionField}, + }, + Item: &pb.ManagedGroup{ + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + }, + }, + err: handlers.ApiErrorWithCode(codes.NotFound), + errContains: "Resource not found.", + }, + { + name: "Cant change Id", + req: &pbs.UpdateManagedGroupRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"id"}, + }, + Item: &pb.ManagedGroup{ + Id: globals.OidcManagedGroupPrefix + "_somethinge", + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "new desc"}, + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "name: \"id\", desc: \"This is a read only field and cannot be specified in an update request.", + }, + { + name: "Cant specify Created Time", + req: &pbs.UpdateManagedGroupRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"created_time"}, + }, + Item: &pb.ManagedGroup{ + CreatedTime: timestamppb.Now(), + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "name: \"created_time\", desc: \"This is a read only field and cannot be specified in an update request.", + }, + { + name: "Cant specify Updated Time", + req: &pbs.UpdateManagedGroupRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"updated_time"}, + }, + Item: &pb.ManagedGroup{ + UpdatedTime: timestamppb.Now(), + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "name: \"updated_time\", desc: \"This is a read only field and cannot be specified in an update request.", + }, + { + name: "Cant specify Type", + req: &pbs.UpdateManagedGroupRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"type"}, + }, + Item: &pb.ManagedGroup{ + Type: "ldap", + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "No valid fields included in the update mask.", + }, + { + name: "Update group names with Bad Value", + req: &pbs.UpdateManagedGroupRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.group_names"}, + }, + Item: &pb.ManagedGroup{ + Attrs: badAttributes, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "name: \"attributes.filter\", desc: \"Field cannot be empty.", + }, + { + name: "Update group names With Good Value", + req: &pbs.UpdateManagedGroupRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.group_names"}, + }, + Item: &pb.ManagedGroup{ + Attrs: modifiedAttributes, + }, + }, + res: &pbs.UpdateManagedGroupResponse{ + Item: &pb.ManagedGroup{ + AuthMethodId: am.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "default"}, + Description: &wrapperspb.StringValue{Value: "default"}, + Type: ldap.Subtype.String(), + Attrs: modifiedAttributes, + Scope: defaultScopeInfo, + AuthorizedActions: ldapAuthorizedActions, + }, + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + mg, cleanup := freshManagedGroup(t) + defer cleanup() + + tc.req.Item.Version = 1 + + if tc.req.GetId() == "" { + tc.req.Id = mg.GetPublicId() + } + + if tc.res != nil && tc.res.Item != nil { + tc.res.Item.Id = mg.GetPublicId() + tc.res.Item.CreatedTime = mg.GetCreateTime().GetTimestamp() + } + + got, gErr := tested.UpdateManagedGroup(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req) + if tc.err != nil { + require.Error(gErr) + assert.True(errors.Is(gErr, tc.err), "UpdateManagedGroup(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) + require.NotEmpty(tc.errContains) + assert.Contains(gErr.Error(), tc.errContains) + } else { + require.NoError(gErr) + } + + if tc.res == nil { + require.Nil(got) + } + + if got != nil { + assert.NotNilf(tc.res, "Expected UpdateManagedGroup response to be nil, but was %v", got) + gotUpdateTime := got.GetItem().GetUpdatedTime() + require.NoError(err, "Error converting proto to timestamp") + + created := mg.GetCreateTime().GetTimestamp() + require.NoError(err, "Error converting proto to timestamp") + + // Verify it is a auth_method updated after it was created + assert.True(gotUpdateTime.AsTime().After(created.AsTime()), "Updated account should have been updated after it's creation. Was updated %v, which is after %v", gotUpdateTime, created) + + // Clear all values which are hard to compare against. + got.Item.UpdatedTime, tc.res.Item.UpdatedTime = nil, nil + + assert.EqualValues(2, got.Item.Version) + tc.res.Item.Version = 2 + } + assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "UpdateManagedGroup(%q) got response %q, wanted %q", tc.req, got, tc.res) + }) + } +} diff --git a/internal/daemon/controller/handlers/managed_groups/validate_test.go b/internal/daemon/controller/handlers/managed_groups/validate_test.go index 1758a4a8ed..8280826098 100644 --- a/internal/daemon/controller/handlers/managed_groups/validate_test.go +++ b/internal/daemon/controller/handlers/managed_groups/validate_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" @@ -78,6 +79,48 @@ func TestValidateCreateRequest(t *testing.T) { }, }, }, + { + name: "mismatched ldap authmethod pw type", + item: &pb.ManagedGroup{ + Type: "oidc", + AuthMethodId: globals.LdapAuthMethodPrefix + "_1234567890", + }, + errContains: fieldError(globals.TypeField, "Doesn't match the parent resource's type."), + }, + { + name: "missing ldap attributes", + item: &pb.ManagedGroup{ + Type: ldap.Subtype.String(), + AuthMethodId: globals.LdapAuthMethodPrefix + "_1234567890", + Attrs: nil, + }, + errContains: fieldError(globals.AttributesField, "Attribute fields is required."), + }, + { + name: "bad ldap attributes", + item: &pb.ManagedGroup{ + Type: ldap.Subtype.String(), + AuthMethodId: globals.LdapAuthMethodPrefix + "_1234567890", + Attrs: &pb.ManagedGroup_LdapManagedGroupAttributes{ + LdapManagedGroupAttributes: &pb.LdapManagedGroupAttributes{ + GroupNames: []string{}, + }, + }, + }, + errContains: "name: \"attributes.group_names\", desc: \"This field is required.", + }, + { + name: "no ldap errors", + item: &pb.ManagedGroup{ + Type: ldap.Subtype.String(), + AuthMethodId: globals.LdapAuthMethodPrefix + "_1234567890", + Attrs: &pb.ManagedGroup_LdapManagedGroupAttributes{ + LdapManagedGroupAttributes: &pb.LdapManagedGroupAttributes{ + GroupNames: []string{"admin"}, + }, + }, + }, + }, } for _, tc := range cases { tc := tc // capture range variable @@ -113,7 +156,7 @@ func TestValidateUpdateRequest(t *testing.T) { errContains: fieldError(globals.TypeField, "Cannot modify the resource type."), }, { - name: "no error", + name: "oidc no error", req: &pbs.UpdateManagedGroupRequest{ Id: globals.OidcManagedGroupPrefix + "_1234567890", UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{}}, @@ -127,6 +170,31 @@ func TestValidateUpdateRequest(t *testing.T) { }, }, }, + { + name: "ldap to password change type", + req: &pbs.UpdateManagedGroupRequest{ + Id: globals.LdapManagedGroupPrefix + "_1234567890", + Item: &pb.ManagedGroup{ + Type: password.Subtype.String(), + }, + }, + errContains: fieldError(globals.TypeField, "Cannot modify the resource type."), + }, + { + name: "ldap no error", + req: &pbs.UpdateManagedGroupRequest{ + Id: globals.LdapManagedGroupPrefix + "_1234567890", + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{}}, + Item: &pb.ManagedGroup{ + Version: 1, + Attrs: &pb.ManagedGroup_LdapManagedGroupAttributes{ + LdapManagedGroupAttributes: &pb.LdapManagedGroupAttributes{ + GroupNames: []string{"admin"}, + }, + }, + }, + }, + }, } for _, tc := range cases { tc := tc // capture range variable diff --git a/internal/daemon/controller/handlers/scopes/scope_service.go b/internal/daemon/controller/handlers/scopes/scope_service.go index a7dae45b71..bc2cd53bd0 100644 --- a/internal/daemon/controller/handlers/scopes/scope_service.go +++ b/internal/daemon/controller/handlers/scopes/scope_service.go @@ -572,7 +572,7 @@ func (s Service) updateInRepo(ctx context.Context, parentScope *pb.ScopeInfo, sc opts = append(opts, iam.WithName(scopeName)) } if primaryAuthMethodId := item.GetPrimaryAuthMethodId(); primaryAuthMethodId != nil { - if !handlers.ValidId(handlers.Id(primaryAuthMethodId.GetValue()), globals.PasswordAuthMethodPrefix, globals.OidcAuthMethodPrefix) { + if !handlers.ValidId(handlers.Id(primaryAuthMethodId.GetValue()), globals.PasswordAuthMethodPrefix, globals.OidcAuthMethodPrefix, globals.LdapAuthMethodPrefix) { return nil, handlers.InvalidArgumentErrorf("Error in provided request.", map[string]string{"primary_auth_method_id": "Improperly formatted identifier"}) } scopePrimaryAuthMethodId = primaryAuthMethodId.GetValue() @@ -929,7 +929,7 @@ func validateUpdateRequest(req *pbs.UpdateScopeRequest) error { if item.GetUpdatedTime() != nil { badFields["updated_time"] = "This is a read only field and cannot be specified in an update request." } - if item.GetPrimaryAuthMethodId().GetValue() != "" && !handlers.ValidId(handlers.Id(item.GetPrimaryAuthMethodId().GetValue()), globals.PasswordAuthMethodPrefix, globals.OidcAuthMethodPrefix) { + if item.GetPrimaryAuthMethodId().GetValue() != "" && !handlers.ValidId(handlers.Id(item.GetPrimaryAuthMethodId().GetValue()), globals.PasswordAuthMethodPrefix, globals.OidcAuthMethodPrefix, globals.LdapAuthMethodPrefix) { badFields["primary_auth_method_id"] = "Improperly formatted identifier." } if len(badFields) > 0 { diff --git a/internal/daemon/controller/handlers/targets/tcp/target_service_test.go b/internal/daemon/controller/handlers/targets/tcp/target_service_test.go index a79423f3e2..c4664cecf8 100644 --- a/internal/daemon/controller/handlers/targets/tcp/target_service_test.go +++ b/internal/daemon/controller/handlers/targets/tcp/target_service_test.go @@ -18,6 +18,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/authtoken" @@ -2438,6 +2439,9 @@ func TestAuthorizeSession(t *testing.T) { oidcAuthRepoFn := func() (*oidc.Repository, error) { return oidc.NewRepository(ctx, rw, rw, kms) } + ldapAuthRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kms) + } plg := host.TestPlugin(t, conn, "test") plgm := map[string]plgpb.HostPluginServiceClient{ @@ -2485,6 +2489,7 @@ func TestAuthorizeSession(t *testing.T) { serversRepoFn, passwordAuthRepoFn, oidcAuthRepoFn, + ldapAuthRepoFn, kms, &authpb.RequestInfo{ Token: at.GetToken(), diff --git a/internal/daemon/controller/interceptor.go b/internal/daemon/controller/interceptor.go index a221c188ab..4182dc78b4 100644 --- a/internal/daemon/controller/interceptor.go +++ b/internal/daemon/controller/interceptor.go @@ -53,6 +53,7 @@ func requestCtxInterceptor( serversRepoFn common.ServersRepoFactory, passwordAuthRepoFn common.PasswordAuthRepoFactory, oidcAuthRepoFn common.OidcAuthRepoFactory, + ldapAuthRepoFn common.LdapAuthRepoFactory, kms *kms.Kms, ticket string, eventer *event.Eventer, @@ -110,7 +111,7 @@ func requestCtxInterceptor( return nil, errors.New(interceptorCtx, errors.Internal, op, "Invalid context (bad ticket)") } - interceptorCtx = auth.NewVerifierContextWithAccounts(interceptorCtx, iamRepoFn, authTokenRepoFn, serversRepoFn, passwordAuthRepoFn, oidcAuthRepoFn, kms, &requestInfo) + interceptorCtx = auth.NewVerifierContextWithAccounts(interceptorCtx, iamRepoFn, authTokenRepoFn, serversRepoFn, passwordAuthRepoFn, oidcAuthRepoFn, ldapAuthRepoFn, kms, &requestInfo) // Add general request information to the context. The information from // the auth verifier context is pretty specifically curated to diff --git a/internal/daemon/controller/interceptor_test.go b/internal/daemon/controller/interceptor_test.go index adff98b600..250fb8170b 100644 --- a/internal/daemon/controller/interceptor_test.go +++ b/internal/daemon/controller/interceptor_test.go @@ -314,7 +314,7 @@ func Test_requestCtxInterceptor(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - interceptor, err := requestCtxInterceptor(factoryCtx, tt.iamRepoFn, tt.authTokenRepoFn, tt.serversRepoFn, nil, nil, tt.kms, tt.ticket, tt.eventer) + interceptor, err := requestCtxInterceptor(factoryCtx, tt.iamRepoFn, tt.authTokenRepoFn, tt.serversRepoFn, nil, nil, nil, tt.kms, tt.ticket, tt.eventer) if tt.wantFactoryErr { require.Error(err) assert.Nil(interceptor) diff --git a/internal/daemon/controller/listeners.go b/internal/daemon/controller/listeners.go index 084303d59d..6dcc761172 100644 --- a/internal/daemon/controller/listeners.go +++ b/internal/daemon/controller/listeners.go @@ -46,7 +46,7 @@ func closeListener(_ context.Context, l net.Listener, _ any) error { func (c *Controller) startListeners() error { servers := make([]func(), 0, len(c.conf.Listeners)) - grpcServer, gwTicket, err := newGrpcServer(c.baseContext, c.IamRepoFn, c.AuthTokenRepoFn, c.ServersRepoFn, c.PasswordAuthRepoFn, c.OidcRepoFn, c.kms, c.conf.Eventer) + grpcServer, gwTicket, err := newGrpcServer(c.baseContext, c.IamRepoFn, c.AuthTokenRepoFn, c.ServersRepoFn, c.PasswordAuthRepoFn, c.OidcRepoFn, c.LdapRepoFn, c.kms, c.conf.Eventer) if err != nil { return fmt.Errorf("failed to create new grpc server: %w", err) } diff --git a/internal/db/schema/migrations/oss/postgres/14/01_wh_user_dimension_oidc.up.sql b/internal/db/schema/migrations/oss/postgres/14/01_wh_user_dimension_oidc.up.sql index 44188e0ba6..fad1232c82 100644 --- a/internal/db/schema/migrations/oss/postgres/14/01_wh_user_dimension_oidc.up.sql +++ b/internal/db/schema/migrations/oss/postgres/14/01_wh_user_dimension_oidc.up.sql @@ -39,6 +39,7 @@ begin; alter column auth_account_email type wh_dim_text ; +-- Replaced in 64/01_wh_user_dimension_ldap.up.sql drop view whx_user_dimension_source; create view whx_user_dimension_source as select -- id is the first column in the target view diff --git a/internal/db/schema/migrations/oss/postgres/65/01_ldap.up.sql b/internal/db/schema/migrations/oss/postgres/65/01_ldap.up.sql new file mode 100644 index 0000000000..294f3d67d4 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/65/01_ldap.up.sql @@ -0,0 +1,644 @@ +-- Copyright (c) HashiCorp, Inc. +-- SPDX-License-Identifier: MPL-2.0 + +begin; + +create table auth_ldap_method_state_enm ( + name text primary key + constraint name_only_predefined_ldap_method_states_allowed + check (name in ('inactive', 'active-private', 'active-public')) +); +comment on table auth_ldap_method_state_enm is +'auth_ldap_method_state_enm entries enumerate the valid auth ldap method states'; + +-- populate the values of auth_ldap_method_state_enm +insert into auth_ldap_method_state_enm(name) + values + ('inactive'), + ('active-private'), + ('active-public'); + +create table auth_ldap_method ( + public_id wt_public_id primary key, + scope_id wt_scope_id not null, + name wt_name, + description wt_description, + create_time wt_timestamp, + update_time wt_timestamp, + version wt_version, + state text not null + constraint auth_ldap_method_state_enm_fkey + references auth_ldap_method_state_enm(name) + on delete restrict + on update cascade, + start_tls bool not null default false, + insecure_tls bool not null default false, + discover_dn bool not null default false, + anon_group_search bool not null default false, + upn_domain text + constraint upn_domain_too_short + check (length(trim(upn_domain)) > 0) + constraint upn_domain_too_long + check (length(trim(upn_domain)) < 253), + enable_groups bool not null default false, + use_token_groups bool not null default false, + constraint auth_method_fkey + foreign key (scope_id, public_id) + references auth_method (scope_id, public_id) + on delete cascade + on update cascade, + constraint auth_ldap_method_scope_id_name_uq + unique(scope_id, name), + constraint auth_ldap_method_scope_id_public_id_uq + unique(scope_id, public_id) +); +comment on table auth_ldap_method is +'auth_ldap_method entries are the current ldap auth methods configured for ' +'existing scopes'; + +-- auth_ldap_method column triggers +create trigger insert_auth_method_subtype before insert on auth_ldap_method + for each row execute procedure insert_auth_method_subtype(); + +create trigger delete_auth_method_subtype after delete on auth_ldap_method + for each row execute procedure delete_auth_method_subtype(); + +create trigger update_auth_method_subtype before update on auth_ldap_method + for each row execute procedure update_auth_method_subtype(); + +create trigger update_time_column before update on auth_ldap_method + for each row execute procedure update_time_column(); + +create trigger immutable_columns before update on auth_ldap_method + for each row execute procedure immutable_columns('public_id', 'scope_id', 'create_time'); + +create trigger default_create_time_column before insert on auth_ldap_method + for each row execute procedure default_create_time(); + +create trigger update_version_column after update on auth_ldap_method + for each row execute procedure update_version_column(); + +-- auth_ldap_url entries are LDAP URLs that specify an LDAP servers to connect +-- to. Examples: ldap://ldap.myorg.com, ldaps://ldap.myorg.com:636. There must +-- be at least one and if there's more than one URL configured for an auth +-- method, the directories will be tried in connection_priority order if there +-- are errors during the connection process. The URL scheme must be either ldap +-- or ldaps. The port is optional.If no port is specified, then a default of 389 +-- is used for ldap and a default of 689 is used for ldaps. (see rfc4516 for +-- more information about LDAP URLs) +-- +-- Updates will be implemented as a delete + insert with the auth_ldap_method +-- being used as the root aggregate for auth_ldap_url updates. +create table auth_ldap_url ( + create_time wt_timestamp, + ldap_method_id wt_public_id not null + constraint auth_ldap_method_fkey + references auth_ldap_method(public_id) + on delete cascade + on update cascade, + url text not null + constraint url_too_short + check (length(trim(url)) > 3) + constraint url_too_long + check (length(trim(url)) < 4000), + constraint url_invalid_protocol + check (url ~ 'ldaps?:\/\/*'), + connection_priority int not null + constraint connection_priority_less_than_one + check(connection_priority >= 1), + primary key(ldap_method_id, connection_priority) +); +comment on table auth_ldap_url is +'auth_ldap_url entries specify a connection URL an LDAP'; + +create function auth_ldap_url_parent_children() returns trigger +as $$ +declare + n integer; +begin + if tg_op = 'INSERT' or tg_op = 'UPDATE' then + select into n count(*) from auth_ldap_url where ldap_method_id = new.ldap_method_id; + if n < 1 then + raise exception 'During % of auth_ldap_url: auth_ldap_method id=% must have at least one url, not %',tg_op,new.ldap_method_id,n; + end if; + end if; + if tg_op = 'UPDATE' then + select into n count(*) from auth_ldap_url where ldap_method_id = old.ldap_method_id; + if n < 1 then + raise exception 'During % of %: auth_ldap_method id=% must have at least one url, not %',tg_op,tg_table_name,old.ldap_method_id,n; + end if; + end if; + + return null; +end; +$$ language plpgsql; +comment on function auth_ldap_url_parent_children() is +'function used on auth_ldap_url after insert/update initially deferred to ensure each ' +'auth_ldap_method has at least one auth_ldap_url. Unfortunately, it cannot be used on ' +'delete since that would make it impossible to delete an ldap auth method, because you ' +'would not be able to remove all of its urls'; + +create constraint trigger auth_ldap_url_children_per_parent_tg + after insert or update or delete on auth_ldap_url deferrable initially deferred + for each row execute procedure auth_ldap_url_parent_children(); + +create function auth_ldap_method_children() returns trigger +as $$ +declare + n integer; +begin + if tg_op = 'INSERT' then + select into n count(*) from auth_ldap_url where ldap_method_id = new.public_id; + if n < 1 then + raise exception 'During % of auth_ldap_method public_id=% must have at least one url, not %',tg_op,new.public_id,n; + end if; + -- No need for an UPDATE or DELETE check, as regular referential integrity constraints + -- and the trigger on `child' will do the job. + + return null; + end if; +end; +$$ language plpgsql; +comment on function auth_ldap_method_children() is +'function used on auth_ldap_method after insert initially deferred to ensure each ' +'auth_ldap_method has at least one auth_ldap_url'; + +create constraint trigger auth_ldap_method_children_tg + after insert on auth_ldap_method deferrable initially deferred + for each row execute procedure auth_ldap_method_children(); + +-- auth_ldap_user_entry_search entries specify the required parameters to find a +-- user entry before attempting to authenticate the user. +-- +-- Updates will be implemented as a delete + insert with the auth_ldap_method +-- being used as the root aggregate for auth_ldap_user_entry_search updates. +create table auth_ldap_user_entry_search ( + create_time wt_timestamp, + ldap_method_id wt_public_id primary key + constraint auth_ldap_method_fkey + references auth_ldap_method(public_id) + on delete cascade + on update cascade, + user_dn text + constraint user_dn_too_short + check (length(trim(user_dn)) > 0) + constraint user_dn_too_long + check (length(trim(user_dn)) < 1025), + user_attr text + constraint user_attr_too_short + check (length(trim(user_attr)) > 0) + constraint user_attr_too_long + check (length(trim(user_attr)) < 1025), + user_filter text + constraint user_filter_too_short + check (length(trim(user_filter)) > 0) + constraint user_filter_too_long + check (length(trim(user_filter)) < 2049), + constraint all_fields_are_not_null + check ( + not(user_dn, user_attr, user_filter) is null + ) +); +comment on table auth_ldap_user_entry_search is +'auth_ldap_user_entry_search entries specify the required parameters to find ' +'a user entry before attempting to authenticate the user'; + +-- auth_ldap_group_entry_search entries specify the required parameters to find +-- the groups a user is a member of +-- +-- Updates will be implemented as a delete + insert with the auth_ldap_method +-- being used as the root aggregate for auth_ldap_user_entry_search updates. +create table auth_ldap_group_entry_search ( + create_time wt_timestamp, + ldap_method_id wt_public_id primary key + constraint auth_ldap_method_fkey + references auth_ldap_method(public_id) + on delete cascade + on update cascade, + group_dn text not null -- required + constraint group_dn_too_short + check (length(trim(group_dn)) > 0) + constraint group_dn_too_long + check (length(trim(group_dn)) < 1025), + group_attr text + constraint group_attr_too_short + check (length(trim(group_attr)) > 0) + constraint group_attr_too_long + check (length(trim(group_attr)) < 1025), + group_filter text + constraint group_filter_too_short + check (length(trim(group_filter)) > 0) + constraint group_filter_too_long + check (length(trim(group_filter)) < 2049) +); +comment on table auth_ldap_group_entry_search is +'auth_ldap_group_entry_search entries specify the required parameters to find ' +'the groups a user is a member of'; + +create function auth_ldap_method_group_search() returns trigger +as $$ +declare + n integer; +begin + if new.enable_groups = true and new.use_token_groups = false then + select into n count(*) from auth_ldap_group_entry_search where ldap_method_id = new.public_id; + if n < 1 then + raise exception 'During % of auth_ldap_method public_id=% must have a configured group_dn when enable_groups = true and use_token_groups = false',tg_op,new.public_id; + end if; + end if; + return null; +end; +$$ language plpgsql; +comment on function auth_ldap_method_children() is +'function used on auth_ldap_method after insert/update initially deferred to ensure each ' +'groups search is properly configured when enable_groups is true and use_token_groups is false'; + +create constraint trigger auth_ldap_method_group_search + after insert or update on auth_ldap_method deferrable initially deferred + for each row execute procedure auth_ldap_method_group_search(); + +-- auth_ldap_certificate entries are optional PEM encoded x509 certificates. +-- Each entry is a single certificate. An ldap auth method may have 0 or more +-- of these optional x509s. If an auth method has any cert entries, they are +-- used as trust anchors when connecting to the auth method's ldap provider +-- (instead of the host system's cert chain). +create table auth_ldap_certificate ( + create_time wt_timestamp, + ldap_method_id wt_public_id not null + constraint auth_ldap_method_fkey + references auth_ldap_method(public_id) + on delete cascade + on update cascade, + certificate bytea not null + constraint certificate_must_not_be_empty + check(length(certificate) > 0), + primary key(ldap_method_id, certificate) +); +comment on table auth_ldap_certificate is + 'auth_ldap_certificate entries are optional PEM encoded x509 certificates. ' + 'Each entry is a single certificate. An ldap auth method may have 0 or more ' + 'of these optional x509s. If an auth method has any cert entries, they are ' + 'used as trust anchors when connecting to the auth methods ldap provider ' + '(instead of the host system cert chain)'; + +create table auth_ldap_client_certificate ( + create_time wt_timestamp, + ldap_method_id wt_public_id primary key + constraint auth_ldap_method_fkey + references auth_ldap_method (public_id) + on delete cascade + on update cascade, + certificate bytea not null -- PEM encoded certificate + constraint certificate_must_not_be_empty + check(length(certificate) > 0), + certificate_key bytea not null -- encrypted PEM encoded private key for certificate + constraint certificate_key_must_not_be_empty + check(length(certificate_key) > 0), + certificate_key_hmac bytea not null + constraint certificate_key_hmac_must_not_be_empty + check(length(certificate_key_hmac) > 0), + key_id text not null + constraint kms_data_key_version_fkey + references kms_data_key_version (private_id) + on delete restrict + on update cascade + ); + comment on table auth_ldap_client_certificate is + 'auth_ldap_client_certificate entries contains a client certificate that a ' + 'auth_ldap_method uses for mTLS when connecting to an LDAP server. ' + 'An auth_ldap_method can have 0 or 1 client certificates.'; + +create table auth_ldap_bind_credential ( + create_time wt_timestamp, + ldap_method_id wt_public_id primary key + constraint auth_ldap_method_fkey + references auth_ldap_method (public_id) + on delete cascade + on update cascade, + dn text not null + constraint dn_too_short + check (length(trim(dn)) > 0) + constraint dn_too_long + check (length(trim(dn)) < 2049), + password bytea not null + constraint password_not_empty + check(length(password) > 0), -- encrypted password0 + password_hmac bytea not null + constraint password_hmac_not_empty + check(length(password_hmac) > 0), + key_id text not null + constraint kms_data_key_version_fkey + references kms_data_key_version (private_id) + on delete restrict + on update cascade +); +comment on table auth_ldap_bind_credential is +'auth_ldap_bind_credential entries allow Boundary to bind (aka authenticate) using ' +'the provided credentials when searching for the user entry used to authenticate.'; + +-- auth_ldap_account_attribute_map entries are the optional attribute maps from custom +-- attributes to the standard attribute of fullname and email. There can be 0 or more +-- for each parent ldap auth method. +create table auth_ldap_account_attribute_map ( + create_time wt_timestamp, + ldap_method_id wt_public_id + constraint auth_ldap_method_fkey + references auth_ldap_method(public_id) + on delete cascade + on update cascade, + from_attribute text not null + constraint from_attribute_must_not_be_empty + check(length(trim(from_attribute)) > 0) + constraint from_attribute_must_be_less_than_1024_chars + check(length(trim(from_attribute)) < 1024), + to_attribute text not null + constraint to_attribute_valid_values + check (lower(to_attribute) in ('fullname', 'email')), -- intentionally case-sensitive matching + primary key(ldap_method_id, to_attribute) +); +comment on table auth_ldap_account_attribute_map is + 'auth_ldap_account_attribute_map entries are the optional attribute maps from custom attributes to ' + 'the standard attributes of sub, name and email. There can be 0 or more for each parent ldap auth method.'; + +create trigger default_create_time_column before insert on auth_ldap_account_attribute_map + for each row execute procedure default_create_time(); + +create trigger immutable_columns before update on auth_ldap_account_attribute_map + for each row execute procedure immutable_columns('ldap_method_id', 'from_attribute', 'to_attribute', 'create_time'); + +create table auth_ldap_account ( + public_id wt_public_id primary key, + auth_method_id wt_public_id not null, + -- NOTE(mgaffney): The scope_id type is not wt_scope_id because the domain + -- check is executed before the insert trigger which retrieves the scope_id + -- causing an insert to fail. + scope_id text not null, + name wt_name, + description wt_description, + create_time wt_timestamp, + update_time wt_timestamp, + version wt_version, + login_name text not null + constraint login_name_must_be_lowercase + check(lower(trim(login_name)) = login_name) + constraint login_name_must_not_be_empty + check(length(trim(login_name)) > 0), + email wt_email, + full_name wt_full_name, + dn text -- will be null until the first successful authentication + constraint dn_must_not_be_empty + check(length(trim(dn)) > 0), + member_of_groups jsonb -- will be null until the first successful authentication + constraint member_of_groups_must_not_be_empty + check(length(trim(member_of_groups::text)) > 0), + constraint auth_ldap_method_fkey + foreign key (scope_id, auth_method_id) + references auth_ldap_method (scope_id, public_id) + on delete cascade + on update cascade, + constraint auth_account_fkey + foreign key (scope_id, auth_method_id, public_id) + references auth_account (scope_id, auth_method_id, public_id) + on delete cascade + on update cascade, + constraint auth_ldap_account_auth_method_id_name_uq + unique(auth_method_id, name), + constraint auth_ldap_account_auth_method_id_login_name_uq + unique(auth_method_id, login_name), + constraint auth_ldap_account_auth_method_id_dn_uq + unique(auth_method_id, dn), + constraint auth_ldap_account_auth_method_id_public_id_uq + unique(auth_method_id, public_id) +); +comment on table auth_ldap_account is +'auth_ldap_account entries are subtypes of auth_account and represent an ldap account.'; + +-- insert_auth_ldap_account_subtype is intended as a before insert +-- trigger on auth_ldap_account. Its purpose is to insert a base +-- auth_account for new ldap accounts. It's a bit different than the +-- standard trigger for this, because it will have conflicting PKs +-- and we just want to "do nothing" on those conflicts, deferring the +-- raising on an error to insert into the auth_ldap_account table. +-- this is all necessary because of we're using predictable public ids +-- for ldap accounts. +create or replace function insert_auth_ldap_account_subtype() returns trigger +as $$ +begin + select auth_method.scope_id + into new.scope_id + from auth_method + where auth_method.public_id = new.auth_method_id; + + insert into auth_account + (public_id, auth_method_id, scope_id) + values + (new.public_id, new.auth_method_id, new.scope_id) + on conflict do nothing; + + return new; +end; + $$ language plpgsql; + +create trigger insert_auth_ldap_account_subtype before insert on auth_ldap_account + for each row execute procedure insert_auth_ldap_account_subtype(); + +create trigger delete_auth_account_subtype after delete on auth_ldap_account + for each row execute procedure delete_auth_account_subtype(); + +create trigger update_time_column before update on auth_ldap_account + for each row execute procedure update_time_column(); + +create trigger immutable_columns before update on auth_ldap_account + for each row execute procedure immutable_columns('public_id', 'auth_method_id', 'scope_id', 'create_time', 'login_name'); + +create trigger default_create_time_column before insert on auth_ldap_account + for each row execute procedure default_create_time(); + +create trigger update_version_column after update on auth_ldap_account + for each row execute procedure update_version_column(); + + +insert into oplog_ticket (name, version) +values + ('auth_ldap_method', 1), -- auth_ldap_method is the root aggregate itself and all of its value objects. + ('auth_ldap_account', 1), + ('auth_ldap_managed_group', 1); + + +-- ldap_auth_method_with_value_obj is useful for reading an ldap auth method +-- with its associated value objects (urls, certs, search config, etc). The use +-- of the postgres string_agg(...) to aggregate the url and cert value objects +-- into a column works because we are only pulling in one column from the +-- associated tables and that value is part of the primary key and unique. This +-- view will make things like recursive listing of ldap auth methods fairly +-- straightforward to implement for the ldap repo. The view also includes an +-- is_primary_auth_method bool +create view ldap_auth_method_with_value_obj as +select + case when s.primary_auth_method_id is not null then + true + else false end + as is_primary_auth_method, + am.public_id, + am.scope_id, + am.name, + am.description, + am.create_time, + am.update_time, + am.version, + am.state, + am.start_tls, + am.insecure_tls, + am.discover_dn, + am.anon_group_search, + am.upn_domain, + am.enable_groups, + am.use_token_groups, + -- the string_agg(..) column will be null if there are no associated value objects + string_agg(distinct url.url, '|') as urls, + string_agg(distinct cert.certificate, '|') as certs, + string_agg(distinct concat_ws('=', aam.from_attribute, aam.to_attribute), '|') as account_attribute_map, + + -- the rest of the fields are zero to one relationships that are stored in + -- related tables. Since we're outer joining with these tables, we need to + -- either add them to the group by, use an aggregating func, or handle + -- multiple rows returning for each auth method. I've chosen to just use + -- string_agg(...) + string_agg(distinct uc.user_dn, '|') as user_dn, + string_agg(distinct uc.user_attr, '|') as user_attr, + string_agg(distinct uc.user_filter, '|') as user_filter, + string_agg(distinct gc.group_dn, '|') as group_dn, + string_agg(distinct gc.group_attr, '|') as group_attr, + string_agg(distinct gc.group_filter, '|') as group_filter, + string_agg(distinct cc.certificate_key, '|') as client_certificate_key, + string_agg(distinct cc.certificate_key_hmac, '|') as client_certificate_key_hmac, + string_agg(distinct cc.key_id, '|') as client_certificate_key_id, + string_agg(distinct cc.certificate, '|') as client_certificate_cert, + string_agg(distinct bc.dn, '|') as bind_dn, + string_agg(distinct bc.password, '|') as bind_password, + string_agg(distinct bc.password_hmac, '|') as bind_password_hmac, + string_agg(distinct bc.key_id, '|') as bind_password_key_id +from + auth_ldap_method am + left outer join iam_scope s on am.public_id = s.primary_auth_method_id + left outer join auth_ldap_url url on am.public_id = url.ldap_method_id + left outer join auth_ldap_certificate cert on am.public_id = cert.ldap_method_id + left outer join auth_ldap_account_attribute_map aam on am.public_id = aam.ldap_method_id + left outer join auth_ldap_user_entry_search uc on am.public_id = uc.ldap_method_id + left outer join auth_ldap_group_entry_search gc on am.public_id = gc.ldap_method_id + left outer join auth_ldap_client_certificate cc on am.public_id = cc.ldap_method_id + left outer join auth_ldap_bind_credential bc on am.public_id = bc.ldap_method_id +group by am.public_id, is_primary_auth_method; -- there can be only one public_id + is_primary_auth_method, so group by isn't a problem. +comment on view ldap_auth_method_with_value_obj is + 'ldap auth method with its associated value objects (urls, certs, search config, etc)'; + +create table auth_ldap_managed_group ( + public_id wt_public_id primary key, + auth_method_id wt_public_id not null, + name wt_name, + description wt_description, + create_time wt_timestamp, + update_time wt_timestamp, + version wt_version, + group_names jsonb not null + constraint group_names_must_not_be_empty + check(length(trim(group_names::text)) > 0), + constraint auth_ldap_method_fkey + foreign key (auth_method_id) -- fk1 + references auth_ldap_method (public_id) + on delete cascade + on update cascade, + -- Ensure it relates to an abstract managed group + constraint auth_managed_group_fkey + foreign key (auth_method_id, public_id) -- fk2 + references auth_managed_group (auth_method_id, public_id) + on delete cascade + on update cascade, + constraint auth_ldap_managed_group_auth_method_id_name_uq + unique(auth_method_id, name) +); +comment on table auth_ldap_managed_group is +'auth_ldap_managed_group entries are subtypes of auth_managed_group and represent an ldap managed group.'; + +-- Define the immutable fields of auth_ldap_managed_group +create trigger immutable_columns before update on auth_ldap_managed_group + for each row execute procedure immutable_columns('public_id', 'auth_method_id', 'create_time'); + +-- Populate create time on insert +create trigger default_create_time_column before insert on auth_ldap_managed_group + for each row execute procedure default_create_time(); + +-- Generate update time on update +create trigger update_time_column before update on auth_ldap_managed_group + for each row execute procedure update_time_column(); + +-- Update version when something changes +create trigger update_version_column after update on auth_ldap_managed_group + for each row execute procedure update_version_column(); + +-- Add into the base table when inserting into the concrete table +create trigger insert_managed_group_subtype before insert on auth_ldap_managed_group + for each row execute procedure insert_managed_group_subtype(); + +-- Ensure that deletions in the ldap subtype result in deletions to the base +-- table. +create trigger delete_managed_group_subtype after delete on auth_ldap_managed_group + for each row execute procedure delete_managed_group_subtype(); + + +-- auth_ldap_managed_group_member_account uses CTEs to expand and "normalize" the jsonb column +-- containing groups in both the accounts and managed groups, then it joins these +-- "normalized" expressions into a join table of mangagd group member account entries +create view auth_ldap_managed_group_member_account as +with +account(id, group_name) as ( + select + a.public_id, ag.group_name + from + auth_ldap_account a + left join jsonb_array_elements(a.member_of_groups) as ag(group_name) on true +), +groups (create_time, id, group_name) as ( + select + g.create_time, + g.public_id, + mg.group_name + from + auth_ldap_managed_group g + left join jsonb_array_elements(g.group_names) as mg(group_name) on true +) +select distinct + groups.create_time, + account.id as member_id, + groups.id as managed_group_id +from account, groups +where account.group_name = groups.group_name; +comment on view auth_ldap_managed_group_member_account is +'auth_ldap_managed_group_member_account is the join view for ' +'managed ldap groups and accounts'; + + +-- recreate view defined in postgres/9/03_oidc_managed_group_member.up.sql +-- so the new view can include both oidc and ldap managed groups +drop view auth_managed_group_member_account; + +-- create view with both oidc and ldap managed groups; we can replace this view +-- to union with other subtype tables as needed in the future. +create view auth_managed_group_member_account as +select + oidc.create_time, + oidc.managed_group_id, + oidc.member_id +from + auth_oidc_managed_group_member_account oidc +union +select + ldap.create_time, + ldap.managed_group_id, + ldap.member_id +from + auth_ldap_managed_group_member_account ldap; +comment on view auth_managed_group_member_account is +''; + +commit; \ No newline at end of file diff --git a/internal/db/schema/migrations/oss/postgres/65/02_wh_user_dimension_ldap.up.sql b/internal/db/schema/migrations/oss/postgres/65/02_wh_user_dimension_ldap.up.sql new file mode 100644 index 0000000000..63524c9ad3 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/65/02_wh_user_dimension_ldap.up.sql @@ -0,0 +1,93 @@ +-- Copyright (c) HashiCorp, Inc. +-- SPDX-License-Identifier: MPL-2.0 + +begin; + + drop view whx_user_dimension_source; + create view whx_user_dimension_source as + select -- id is the first column in the target view + u.public_id as user_id, + coalesce(u.name, 'None') as user_name, + coalesce(u.description, 'None') as user_description, + coalesce(aa.public_id, 'None') as auth_account_id, + case + when apa.public_id is not null then 'password auth account' + when ala.public_id is not null then 'ldap auth account' + when aoa.public_id is not null then 'oidc auth account' + else 'None' + end as auth_account_type, + case + when apa.public_id is not null then coalesce(apa.name, 'None') + when ala.public_id is not null then coalesce(ala.name, 'None') + when aoa.public_id is not null then coalesce(aoa.name, 'None') + else 'None' + end as auth_account_name, + case + when apa.public_id is not null then coalesce(apa.description, 'None') + when ala.public_id is not null then coalesce(ala.description, 'None') + when aoa.public_id is not null then coalesce(aoa.description, 'None') + else 'None' + end as auth_account_description, + case + when apa.public_id is not null then 'Not Applicable' + when ala.public_id is not null then ala.login_name + when aoa.public_id is not null then aoa.subject + else 'None' + end as auth_account_external_id, + case + when apa.public_id is not null then 'Not Applicable' + when ala.public_id is not null + and ala.full_name is not null then ala.full_name + when aoa.public_id is not null + and aoa.full_name is not null then aoa.full_name + else 'None' + end as auth_account_full_name, + case + when apa.public_id is not null then 'Not Applicable' + when ala.public_id is not null + and ala.email is not null then ala.email + when aoa.public_id is not null + and aoa.email is not null then aoa.email + else 'None' + end as auth_account_email, + coalesce(am.public_id, 'None') as auth_method_id, + case + when apa.public_id is not null then 'password auth method' + when ala.public_id is not null then 'ldap auth method' + when aoa.public_id is not null then 'oidc auth method' + else 'None' + end as auth_method_type, + case + when apm.public_id is not null then coalesce(apm.name, 'None') + when alm.public_id is not null then coalesce(alm.name, 'None') + when aom.public_id is not null then coalesce(aom.name, 'None') + else 'None' + end as auth_method_name, + case + when apm.public_id is not null then coalesce(apm.description, 'None') + when alm.public_id is not null then coalesce(alm.description, 'None') + when aom.public_id is not null then coalesce(aom.description, 'None') + else 'None' + end as auth_method_description, + case + when apa.public_id is not null then 'Not Applicable' + when alm.public_id is not null then 'Not Applicable' + when aom.public_id is null then 'None' + else aom.issuer + end as auth_method_external_id, + org.public_id as user_organization_id, + coalesce(org.name, 'None') as user_organization_name, + coalesce(org.description, 'None') as user_organization_description + from iam_user as u + left join auth_account as aa on u.public_id = aa.iam_user_id + left join auth_method as am on aa.auth_method_id = am.public_id + left join auth_password_account as apa on aa.public_id = apa.public_id + left join auth_password_method as apm on am.public_id = apm.public_id + left join auth_oidc_account as aoa on aa.public_id = aoa.public_id + left join auth_oidc_method as aom on am.public_id = aom.public_id + left join auth_ldap_account as ala on aa.public_id = ala.public_id + left join auth_ldap_method as alm on am.public_id = alm.public_id + join iam_scope as org on u.scope_id = org.public_id + ; + +commit; \ No newline at end of file diff --git a/internal/db/schema/migrations/oss/postgres/9/03_oidc_managed_group_member.up.sql b/internal/db/schema/migrations/oss/postgres/9/03_oidc_managed_group_member.up.sql index d786af6830..ac417c3770 100644 --- a/internal/db/schema/migrations/oss/postgres/9/03_oidc_managed_group_member.up.sql +++ b/internal/db/schema/migrations/oss/postgres/9/03_oidc_managed_group_member.up.sql @@ -34,6 +34,7 @@ create trigger default_create_time_column before insert on auth_oidc_managed_gro create trigger auth_immutable_managed_oidc_group_member_account before update on auth_oidc_managed_group_member_account for each row execute procedure auth_immutable_managed_oidc_group_member_account(); +-- Updated in 64/01_ldap.up.sql -- Initially create the view with just oidc; eventually we can replace this view -- to union with other subtype tables. create view auth_managed_group_member_account as diff --git a/internal/db/sqltest/initdb.d/03_widgets_persona.sql b/internal/db/sqltest/initdb.d/03_widgets_persona.sql index 9b3e2ae5af..e7fa270b9a 100644 --- a/internal/db/sqltest/initdb.d/03_widgets_persona.sql +++ b/internal/db/sqltest/initdb.d/03_widgets_persona.sql @@ -192,6 +192,30 @@ begin; ('kdkv___widget', 'aoa___walter', 'oidc__walter', 'oidc__walter'::bytea), ('kdkv___widget', 'aoa___warren', 'oidc__warren', 'oidc__warren'::bytea); + insert into auth_ldap_method + (scope_id, public_id, name, state) + values + ('o_____widget', 'alm___widget', 'Widget LDAP', 'active-private'); + insert into auth_ldap_url + (ldap_method_id, url, connection_priority) + values + ('alm___widget', 'ldaps://ldap1', 1); + + insert into auth_ldap_account + (auth_method_id, public_id, name, description, full_name, email, login_name) + values + ('alm___widget', 'ala___walter', 'walter account', 'Walter LDAP Account', 'Walter', 'walter@widget.test', 'walter'), + ('alm___widget', 'ala___warren', 'warren account', 'Warren LDAP Account', null, null, 'warren'); + + update auth_account set iam_user_id = 'u_____walter' where public_id = 'ala___walter'; + update auth_account set iam_user_id = 'u_____warren' where public_id = 'ala___warren'; + + insert into auth_token + (key_id, auth_account_id, public_id, token) + values + ('kdkv___widget', 'ala___walter', 'ldap__walter', 'ldap__walter'::bytea), + ('kdkv___widget', 'ala___warren', 'ldap__warren', 'ldap__warren'::bytea); + end; $$ language plpgsql; diff --git a/internal/db/sqltest/tests/account/ldap/account.sql b/internal/db/sqltest/tests/account/ldap/account.sql new file mode 100644 index 0000000000..aaa74bc7bf --- /dev/null +++ b/internal/db/sqltest/tests/account/ldap/account.sql @@ -0,0 +1,38 @@ +-- Copyright (c) HashiCorp, Inc. +-- SPDX-License-Identifier: MPL-2.0 + +-- account tests triggers: +-- insert_auth_account_subtype +-- delete_auth_account_subtype + +begin; +select plan(8); +select wtt_load('widgets', 'iam', 'kms', 'auth'); + +-- validate the setup data +select is(count(*), 1::bigint) from auth_ldap_account where public_id = 'ala___walter'; +select is(count(*), 1::bigint) from auth_account where public_id = 'ala___walter'; + +-- validate the insert triggers +prepare insert_ldap_account as + insert into auth_ldap_account + (auth_method_id, public_id, login_name) + values + ('alm___widget', 'ala___tania', 'tania'); +select lives_ok('insert_ldap_account'); + +select is(count(*), 1::bigint) from auth_ldap_account where public_id = 'ala___tania'; +select is(count(*), 1::bigint) from auth_account where public_id = 'ala___tania'; + +-- validate the delete triggers +prepare delete_ldap_account as + delete + from auth_ldap_account + where public_id = 'ala___tania'; +select lives_ok('delete_ldap_account'); + +select is(count(*), 0::bigint) from auth_ldap_account where public_id = 'ala___tania'; +select is(count(*), 0::bigint) from auth_account where public_id = 'ala___tania'; + +select * from finish(); +rollback; \ No newline at end of file diff --git a/internal/db/sqltest/tests/wh/user_dimension/ldap_auth_new_session.sql b/internal/db/sqltest/tests/wh/user_dimension/ldap_auth_new_session.sql new file mode 100644 index 0000000000..49c8d97a13 --- /dev/null +++ b/internal/db/sqltest/tests/wh/user_dimension/ldap_auth_new_session.sql @@ -0,0 +1,84 @@ +-- Copyright (c) HashiCorp, Inc. +-- SPDX-License-Identifier: MPL-2.0 + +-- oidc_auth_new_session tests the wh_user_dimesion when +-- a new session is created using the oidc auth method. +begin; + select plan(40); + + select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets'); + + -- ensure no existing dimensions + select is(count(*), 0::bigint) from wh_user_dimension where user_organization_id = 'o_____widget'; + + -- insert first session, should result in a new user dimension + insert into session + ( project_id , target_id , user_id , auth_token_id , certificate , endpoint , public_id) + values + ('p____bwidget' , 't_________wb' , 'u_____walter' , 'ldap__walter' , 'abc'::bytea , 'ep1' , 's1____walter'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s1____walter', 's___1wb-sths', 'h_____wb__01'); + + select is(count(*), 1::bigint) from wh_user_dimension where user_id = 'u_____walter'; + select is(user_id, 'u_____walter') from wh_user_dimension where user_id = 'u_____walter'; + select is(user_name, 'Walter') from wh_user_dimension where user_id = 'u_____walter'; + select is(user_description, 'None') from wh_user_dimension where user_id = 'u_____walter'; + + select is(auth_account_id, 'ala___walter') from wh_user_dimension where user_id = 'u_____walter'; + select is(auth_account_type, 'ldap auth account') from wh_user_dimension where user_id = 'u_____walter'; + select is(auth_account_name, 'walter account') from wh_user_dimension where user_id = 'u_____walter'; + select is(auth_account_description, 'Walter LDAP Account') from wh_user_dimension where user_id = 'u_____walter'; + select is(auth_account_external_id, 'walter') from wh_user_dimension where user_id = 'u_____walter'; + select is(auth_account_full_name, 'Walter') from wh_user_dimension where user_id = 'u_____walter'; + select is(auth_account_email, 'walter@widget.test') from wh_user_dimension where user_id = 'u_____walter'; + + select is(auth_method_id, 'alm___widget') from wh_user_dimension where user_id = 'u_____walter'; + select is(auth_method_type, 'ldap auth method') from wh_user_dimension where user_id = 'u_____walter'; + select is(auth_method_name, 'Widget LDAP') from wh_user_dimension where user_id = 'u_____walter'; + select is(auth_method_description, 'None') from wh_user_dimension where user_id = 'u_____walter'; + select is(auth_method_external_id, 'Not Applicable') from wh_user_dimension where user_id = 'u_____walter'; + + select is(user_organization_id, 'o_____widget') from wh_user_dimension where user_id = 'u_____walter'; + select is(user_organization_name, 'Widget Inc') from wh_user_dimension where user_id = 'u_____walter'; + select is(user_organization_description, 'None') from wh_user_dimension where user_id = 'u_____walter'; + + select is(current_row_indicator, 'Current') from wh_user_dimension where user_id = 'u_____walter'; + + -- insert session without full name or email + insert into session + ( project_id , target_id , user_id , auth_token_id , certificate , endpoint , public_id) + values + ('p____bwidget' , 't_________wb' , 'u_____warren' , 'ldap__warren' , 'abc'::bytea , 'ep1' , 's1____warren'); + insert into session_host_set_host + (session_id, host_set_id, host_id) + values + ('s1____warren', 's___1wb-sths', 'h_____wb__01'); + + select is(count(*), 1::bigint) from wh_user_dimension where user_id = 'u_____warren'; + select is(user_name, 'Warren') from wh_user_dimension where user_id = 'u_____warren'; + select is(user_description, 'None') from wh_user_dimension where user_id = 'u_____warren'; + + select is(auth_account_id, 'ala___warren') from wh_user_dimension where user_id = 'u_____warren'; + select is(auth_account_type, 'ldap auth account') from wh_user_dimension where user_id = 'u_____warren'; + select is(auth_account_name, 'warren account') from wh_user_dimension where user_id = 'u_____warren'; + select is(auth_account_description, 'Warren LDAP Account') from wh_user_dimension where user_id = 'u_____warren'; + select is(auth_account_external_id, 'warren') from wh_user_dimension where user_id = 'u_____warren'; + select is(auth_account_full_name, 'None') from wh_user_dimension where user_id = 'u_____warren'; + select is(auth_account_email, 'None') from wh_user_dimension where user_id = 'u_____warren'; + + select is(auth_method_id, 'alm___widget') from wh_user_dimension where user_id = 'u_____warren'; + select is(auth_method_type, 'ldap auth method') from wh_user_dimension where user_id = 'u_____warren'; + select is(auth_method_name, 'Widget LDAP') from wh_user_dimension where user_id = 'u_____warren'; + select is(auth_method_description, 'None') from wh_user_dimension where user_id = 'u_____warren'; + select is(auth_method_external_id, 'Not Applicable') from wh_user_dimension where user_id = 'u_____warren'; + + select is(user_organization_id, 'o_____widget') from wh_user_dimension where user_id = 'u_____warren'; + select is(user_organization_name, 'Widget Inc') from wh_user_dimension where user_id = 'u_____warren'; + select is(user_organization_description, 'None') from wh_user_dimension where user_id = 'u_____warren'; + + select is(current_row_indicator, 'Current') from wh_user_dimension where user_id = 'u_____warren'; + + select * from finish(); +rollback; diff --git a/internal/db/sqltest/tests/wh/user_dimension_views/source.sql b/internal/db/sqltest/tests/wh/user_dimension_views/source.sql index fe96d672e9..5669e1c20d 100644 --- a/internal/db/sqltest/tests/wh/user_dimension_views/source.sql +++ b/internal/db/sqltest/tests/wh/user_dimension_views/source.sql @@ -3,7 +3,7 @@ -- source tests the whx_user_dimension_source view. begin; - select plan(5); + select plan(7); select wtt_load('widgets', 'iam', 'kms', 'auth', 'hosts', 'targets'); -- auth_password_account @@ -68,5 +68,30 @@ begin; where s.user_id = 'u_____warren' and s.auth_account_id = 'aoa___warren'; +-- auth_ldap_account + select is(s.*, row( + 'u_____walter', 'Walter', 'None', + 'ala___walter', 'ldap auth account', 'walter account', 'Walter LDAP Account', + 'walter', 'Walter', 'walter@widget.test', + 'alm___widget', 'ldap auth method', 'Widget LDAP', 'None', + 'Not Applicable', + 'o_____widget', 'Widget Inc', 'None' + )::whx_user_dimension_source) + from whx_user_dimension_source as s + where s.user_id = 'u_____walter' + and s.auth_account_id = 'ala___walter'; + + select is(s.*, row( + 'u_____warren', 'Warren', 'None', + 'ala___warren', 'ldap auth account', 'warren account', 'Warren LDAP Account', + 'warren', 'None', 'None', + 'alm___widget', 'ldap auth method', 'Widget LDAP', 'None', + 'Not Applicable', + 'o_____widget', 'Widget Inc', 'None' + )::whx_user_dimension_source) + from whx_user_dimension_source as s + where s.user_id = 'u_____warren' + and s.auth_account_id = 'ala___warren'; + select * from finish(); rollback; diff --git a/internal/gen/controller/api/services/auth_method_service.pb.go b/internal/gen/controller/api/services/auth_method_service.pb.go index 95226a1757..e06b30cfb0 100644 --- a/internal/gen/controller/api/services/auth_method_service.pb.go +++ b/internal/gen/controller/api/services/auth_method_service.pb.go @@ -855,6 +855,64 @@ func (x *OidcStartAttributes) GetCachedRoundtripPayload() string { return "" } +// The layout of the struct for "attributes" field in AuthenticateRequest for an +// ldap type. This message isn't directly referenced anywhere but is used here +// to define the expected field names and types. +type LdapLoginAttributes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + LoginName string `protobuf:"bytes,10,opt,name=login_name,proto3" json:"login_name,omitempty" class:"sensitive"` // @gotags: `class:"sensitive"` + Password string `protobuf:"bytes,20,opt,name=password,proto3" json:"password,omitempty" class:"secret"` // @gotags: `class:"secret"` +} + +func (x *LdapLoginAttributes) Reset() { + *x = LdapLoginAttributes{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LdapLoginAttributes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LdapLoginAttributes) ProtoMessage() {} + +func (x *LdapLoginAttributes) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[15] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LdapLoginAttributes.ProtoReflect.Descriptor instead. +func (*LdapLoginAttributes) Descriptor() ([]byte, []int) { + return file_controller_api_services_v1_auth_method_service_proto_rawDescGZIP(), []int{15} +} + +func (x *LdapLoginAttributes) GetLoginName() string { + if x != nil { + return x.LoginName + } + return "" +} + +func (x *LdapLoginAttributes) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + type AuthenticateRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -877,6 +935,7 @@ type AuthenticateRequest struct { // *AuthenticateRequest_OidcStartAttributes // *AuthenticateRequest_OidcAuthMethodAuthenticateCallbackRequest // *AuthenticateRequest_OidcAuthMethodAuthenticateTokenRequest + // *AuthenticateRequest_LdapLoginAttributes Attrs isAuthenticateRequest_Attrs `protobuf_oneof:"attrs"` // The command to perform. Command string `protobuf:"bytes,5,opt,name=command,proto3" json:"command,omitempty" class:"public"` // @gotags: `class:"public"` @@ -885,7 +944,7 @@ type AuthenticateRequest struct { func (x *AuthenticateRequest) Reset() { *x = AuthenticateRequest{} if protoimpl.UnsafeEnabled { - mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[15] + mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -898,7 +957,7 @@ func (x *AuthenticateRequest) String() string { func (*AuthenticateRequest) ProtoMessage() {} func (x *AuthenticateRequest) ProtoReflect() protoreflect.Message { - mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[15] + mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -911,7 +970,7 @@ func (x *AuthenticateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AuthenticateRequest.ProtoReflect.Descriptor instead. func (*AuthenticateRequest) Descriptor() ([]byte, []int) { - return file_controller_api_services_v1_auth_method_service_proto_rawDescGZIP(), []int{15} + return file_controller_api_services_v1_auth_method_service_proto_rawDescGZIP(), []int{16} } func (x *AuthenticateRequest) GetAuthMethodId() string { @@ -978,6 +1037,13 @@ func (x *AuthenticateRequest) GetOidcAuthMethodAuthenticateTokenRequest() *authm return nil } +func (x *AuthenticateRequest) GetLdapLoginAttributes() *LdapLoginAttributes { + if x, ok := x.GetAttrs().(*AuthenticateRequest_LdapLoginAttributes); ok { + return x.LdapLoginAttributes + } + return nil +} + func (x *AuthenticateRequest) GetCommand() string { if x != nil { return x.Command @@ -1012,6 +1078,10 @@ type AuthenticateRequest_OidcAuthMethodAuthenticateTokenRequest struct { OidcAuthMethodAuthenticateTokenRequest *authmethods.OidcAuthMethodAuthenticateTokenRequest `protobuf:"bytes,10,opt,name=oidc_auth_method_authenticate_token_request,json=oidcAuthMethodAuthenticateTokenRequest,proto3,oneof"` } +type AuthenticateRequest_LdapLoginAttributes struct { + LdapLoginAttributes *LdapLoginAttributes `protobuf:"bytes,11,opt,name=ldap_login_attributes,json=ldapLoginAttributes,proto3,oneof"` +} + func (*AuthenticateRequest_Attributes) isAuthenticateRequest_Attrs() {} func (*AuthenticateRequest_PasswordLoginAttributes) isAuthenticateRequest_Attrs() {} @@ -1022,6 +1092,8 @@ func (*AuthenticateRequest_OidcAuthMethodAuthenticateCallbackRequest) isAuthenti func (*AuthenticateRequest_OidcAuthMethodAuthenticateTokenRequest) isAuthenticateRequest_Attrs() {} +func (*AuthenticateRequest_LdapLoginAttributes) isAuthenticateRequest_Attrs() {} + type AuthenticateResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1044,7 +1116,7 @@ type AuthenticateResponse struct { func (x *AuthenticateResponse) Reset() { *x = AuthenticateResponse{} if protoimpl.UnsafeEnabled { - mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[16] + mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1057,7 +1129,7 @@ func (x *AuthenticateResponse) String() string { func (*AuthenticateResponse) ProtoMessage() {} func (x *AuthenticateResponse) ProtoReflect() protoreflect.Message { - mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[16] + mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1070,7 +1142,7 @@ func (x *AuthenticateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AuthenticateResponse.ProtoReflect.Descriptor instead. func (*AuthenticateResponse) Descriptor() ([]byte, []int) { - return file_controller_api_services_v1_auth_method_service_proto_rawDescGZIP(), []int{16} + return file_controller_api_services_v1_auth_method_service_proto_rawDescGZIP(), []int{17} } func (x *AuthenticateResponse) GetType() string { @@ -1301,213 +1373,226 @@ var file_controller_api_services_v1_auth_method_service_proto_rawDesc = []byte{ 0x64, 0x5f, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x74, 0x72, 0x69, 0x70, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x63, 0x61, 0x63, 0x68, 0x65, 0x64, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x74, 0x72, 0x69, 0x70, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, - 0x64, 0x22, 0xf4, 0x06, 0x0a, 0x13, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x61, 0x75, 0x74, - 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, - 0x64, 0x12, 0x22, 0x0a, 0x0a, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0a, 0x74, 0x6f, 0x6b, 0x65, 0x6e, - 0x5f, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x61, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x48, 0x00, 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x73, 0x12, 0x83, 0x01, 0x0a, 0x19, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, - 0x64, 0x5f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x65, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, - 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x4c, 0x6f, - 0x67, 0x69, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x42, 0x10, 0xfa, - 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, - 0x00, 0x52, 0x17, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x4c, 0x6f, 0x67, 0x69, 0x6e, - 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x77, 0x0a, 0x15, 0x6f, 0x69, - 0x64, 0x63, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x65, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, - 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x69, 0x64, 0x63, 0x53, 0x74, 0x61, 0x72, 0x74, - 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x42, 0x10, 0xfa, 0xd2, 0xe4, 0x93, - 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x13, - 0x6f, 0x69, 0x64, 0x63, 0x53, 0x74, 0x61, 0x72, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x65, 0x73, 0x12, 0xc9, 0x01, 0x0a, 0x2e, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x61, 0x75, 0x74, - 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, - 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x63, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x5f, 0x72, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x52, 0x2e, 0x63, - 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, - 0x6f, 0x64, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, - 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, - 0x65, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x64, 0x22, 0x51, 0x0a, 0x13, 0x4c, 0x64, 0x61, 0x70, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x41, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x6c, 0x6f, 0x67, 0x69, + 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6c, 0x6f, + 0x67, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, + 0x77, 0x6f, 0x72, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, + 0x77, 0x6f, 0x72, 0x64, 0x22, 0xed, 0x07, 0x0a, 0x13, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, + 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x5f, 0x69, 0x64, 0x12, 0x22, 0x0a, 0x0a, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0a, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x39, 0x0a, 0x0a, + 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x48, 0x00, 0x52, 0x0a, 0x61, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x83, 0x01, 0x0a, 0x19, 0x70, 0x61, 0x73, 0x73, + 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x63, 0x6f, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x42, 0x10, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, - 0x41, 0x4c, 0x48, 0x00, 0x52, 0x29, 0x6f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, - 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, - 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0xc0, 0x01, 0x0a, 0x2b, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, - 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, - 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, - 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x4f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, - 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, - 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x76, 0x31, 0x2e, - 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, - 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x10, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, - 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x26, 0x6f, 0x69, 0x64, 0x63, - 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, - 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x42, 0x07, 0x0a, 0x05, - 0x61, 0x74, 0x74, 0x72, 0x73, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x52, 0x0b, 0x63, 0x72, 0x65, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x22, 0xf8, 0x06, 0x0a, 0x14, 0x41, 0x75, 0x74, - 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, - 0x63, 0x74, 0x48, 0x00, 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, - 0x12, 0xc3, 0x01, 0x0a, 0x2c, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, - 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x50, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, - 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x76, - 0x31, 0x2e, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, - 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, - 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x10, 0xfa, 0xd2, 0xe4, 0x93, 0x02, - 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x27, 0x6f, - 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, - 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xcc, 0x01, 0x0a, 0x2f, 0x6f, 0x69, 0x64, 0x63, 0x5f, + 0x41, 0x4c, 0x48, 0x00, 0x52, 0x17, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x4c, 0x6f, + 0x67, 0x69, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x77, 0x0a, + 0x15, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x63, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x69, 0x64, 0x63, 0x53, 0x74, + 0x61, 0x72, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x42, 0x10, 0xfa, + 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, + 0x00, 0x52, 0x13, 0x6f, 0x69, 0x64, 0x63, 0x53, 0x74, 0x61, 0x72, 0x74, 0x41, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0xc9, 0x01, 0x0a, 0x2e, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x63, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, - 0x6b, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x53, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, - 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, - 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x69, 0x64, 0x63, 0x41, - 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, - 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73, + 0x6b, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x52, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, + 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x42, 0x10, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, + 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x29, 0x6f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, + 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0xc0, 0x01, 0x0a, 0x2b, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x61, 0x75, 0x74, 0x68, + 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x4f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, + 0x76, 0x31, 0x2e, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x10, 0xfa, 0xd2, 0xe4, 0x93, 0x02, + 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x26, 0x6f, + 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, + 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x77, 0x0a, 0x15, 0x6c, 0x64, 0x61, 0x70, 0x5f, 0x6c, 0x6f, + 0x67, 0x69, 0x6e, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x0b, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, + 0x31, 0x2e, 0x4c, 0x64, 0x61, 0x70, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x73, 0x42, 0x10, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, + 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x13, 0x6c, 0x64, 0x61, 0x70, 0x4c, + 0x6f, 0x67, 0x69, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x18, + 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x42, 0x07, 0x0a, 0x05, 0x61, 0x74, 0x74, 0x72, + 0x73, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x52, 0x0b, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x61, 0x6c, 0x73, 0x22, 0xf8, 0x06, 0x0a, 0x14, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, + 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x48, 0x00, + 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0xc3, 0x01, 0x0a, + 0x2c, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x73, + 0x74, 0x61, 0x72, 0x74, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x50, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x61, + 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x69, + 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, + 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x10, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, - 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x2a, 0x6f, 0x69, 0x64, 0x63, 0x41, + 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x27, 0x6f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, - 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xc3, 0x01, 0x0a, 0x2c, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x61, - 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x65, - 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x72, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x50, 0x2e, 0x63, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0xcc, 0x01, 0x0a, 0x2f, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x61, 0x75, 0x74, 0x68, + 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x5f, 0x63, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x5f, 0x72, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x53, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, - 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x10, - 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, - 0x48, 0x00, 0x52, 0x27, 0x6f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, - 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x75, 0x0a, 0x13, 0x61, - 0x75, 0x74, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x65, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x42, 0x10, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, + 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x2a, 0x6f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0xc3, 0x01, 0x0a, 0x2c, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, + 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x50, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x2e, 0x76, - 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x42, 0x10, 0xfa, 0xd2, 0xe4, - 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, - 0x11, 0x61, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x42, 0x07, 0x0a, 0x05, - 0x61, 0x74, 0x74, 0x72, 0x73, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x4a, 0x04, 0x08, 0x02, 0x10, - 0x03, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x52, 0x0a, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x74, - 0x79, 0x70, 0x65, 0x32, 0x95, 0x0b, 0x0a, 0x11, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, - 0x6f, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0xb8, 0x01, 0x0a, 0x0d, 0x47, 0x65, - 0x74, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x30, 0x2e, 0x63, 0x6f, - 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, - 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, - 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x75, - 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x42, 0x92, 0x41, 0x1c, 0x12, 0x1a, 0x47, 0x65, 0x74, 0x73, 0x20, 0x61, 0x20, 0x73, 0x69, - 0x6e, 0x67, 0x6c, 0x65, 0x20, 0x41, 0x75, 0x74, 0x68, 0x20, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, - 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1d, 0x12, 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, - 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x62, 0x04, - 0x69, 0x74, 0x65, 0x6d, 0x12, 0xb0, 0x01, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, - 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x12, 0x32, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, + 0x76, 0x31, 0x2e, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x10, 0xfa, 0xd2, 0xe4, 0x93, + 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x27, + 0x6f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, + 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x75, 0x0a, 0x13, 0x61, 0x75, 0x74, 0x68, 0x5f, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x09, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, + 0x61, 0x75, 0x74, 0x68, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, + 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x42, 0x10, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, + 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x11, 0x61, 0x75, 0x74, + 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, + 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x42, 0x07, 0x0a, 0x05, 0x61, 0x74, 0x74, 0x72, + 0x73, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x52, 0x04, 0x69, + 0x74, 0x65, 0x6d, 0x52, 0x0a, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x32, + 0x95, 0x0b, 0x0a, 0x11, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0xb8, 0x01, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, + 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x30, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x63, 0x6f, 0x6e, 0x74, + 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x42, 0x92, 0x41, + 0x1c, 0x12, 0x1a, 0x47, 0x65, 0x74, 0x73, 0x20, 0x61, 0x20, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, + 0x20, 0x41, 0x75, 0x74, 0x68, 0x20, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x2e, 0x82, 0xd3, 0xe4, + 0x93, 0x02, 0x1d, 0x12, 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, + 0x12, 0xb0, 0x01, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, + 0x68, 0x6f, 0x64, 0x73, 0x12, 0x32, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, + 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, - 0x74, 0x68, 0x6f, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x63, - 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, - 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x34, 0x92, 0x41, 0x19, 0x12, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x20, 0x61, 0x6c, - 0x6c, 0x20, 0x41, 0x75, 0x74, 0x68, 0x20, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x12, 0x10, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, - 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x12, 0xc5, 0x01, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x33, 0x2e, 0x63, + 0x74, 0x68, 0x6f, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x34, 0x92, + 0x41, 0x19, 0x12, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x20, 0x61, 0x6c, 0x6c, 0x20, 0x41, 0x75, + 0x74, 0x68, 0x20, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x12, 0x12, 0x10, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x73, 0x12, 0xc5, 0x01, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x75, + 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, + 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, + 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x46, 0x92, 0x41, 0x1f, 0x12, 0x1d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x73, 0x20, 0x61, 0x20, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x20, 0x41, 0x75, 0x74, 0x68, 0x20, + 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x22, 0x10, 0x2f, + 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x3a, + 0x04, 0x69, 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0xc4, 0x01, 0x0a, 0x10, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x12, 0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, + 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, + 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, + 0x68, 0x6f, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x45, 0x92, 0x41, 0x19, + 0x12, 0x17, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x6e, 0x20, 0x41, 0x75, 0x74, + 0x68, 0x20, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x23, 0x32, + 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, 0x74, + 0x65, 0x6d, 0x12, 0xb6, 0x01, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, + 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x34, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, - 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x46, 0x92, 0x41, 0x1f, 0x12, 0x1d, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x20, 0x41, - 0x75, 0x74, 0x68, 0x20, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, - 0x1e, 0x22, 0x10, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, - 0x6f, 0x64, 0x73, 0x3a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, - 0xc4, 0x01, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, - 0x74, 0x68, 0x6f, 0x64, 0x12, 0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, - 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, - 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, - 0x6f, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x63, 0x6f, 0x6e, 0x74, - 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, - 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x45, 0x92, 0x41, 0x19, 0x12, 0x17, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x6e, - 0x20, 0x41, 0x75, 0x74, 0x68, 0x20, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x2e, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x23, 0x32, 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, 0x65, - 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x04, 0x69, 0x74, 0x65, 0x6d, - 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0xb6, 0x01, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x33, 0x2e, 0x63, 0x6f, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x37, 0x92, 0x41, 0x17, 0x12, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, + 0x20, 0x61, 0x6e, 0x20, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x17, 0x2a, 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0xcf, 0x01, 0x0a, 0x0b, + 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, - 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x34, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, - 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x37, 0x92, 0x41, 0x17, 0x12, 0x15, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x73, 0x20, 0x61, 0x6e, 0x20, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, - 0x6f, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x2a, 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, - 0x74, 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, - 0xcf, 0x01, 0x0a, 0x0b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, - 0x6e, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x2f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, - 0x6e, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x5f, 0x92, 0x41, 0x29, 0x12, 0x27, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x20, 0x74, - 0x68, 0x65, 0x20, 0x73, 0x74, 0x61, 0x74, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x61, 0x6e, 0x20, 0x4f, - 0x49, 0x44, 0x43, 0x20, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x82, 0xd3, - 0xe4, 0x93, 0x02, 0x2d, 0x22, 0x22, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, - 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x63, 0x68, 0x61, 0x6e, - 0x67, 0x65, 0x2d, 0x73, 0x74, 0x61, 0x74, 0x65, 0x3a, 0x01, 0x2a, 0x62, 0x04, 0x69, 0x74, 0x65, - 0x6d, 0x12, 0xf7, 0x01, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x12, 0x2f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, - 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, - 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, - 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x83, 0x01, 0x92, 0x41, 0x47, 0x12, 0x45, 0x41, 0x75, 0x74, - 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x20, 0x61, 0x20, 0x75, 0x73, 0x65, 0x72, - 0x20, 0x74, 0x6f, 0x20, 0x61, 0x6e, 0x20, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x20, 0x61, 0x6e, 0x64, - 0x20, 0x72, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x75, 0x74, - 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x74, 0x6f, 0x6b, 0x65, - 0x6e, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x33, 0x22, 0x2e, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, - 0x74, 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2f, 0x7b, 0x61, 0x75, 0x74, 0x68, - 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x7d, 0x3a, 0x61, 0x75, 0x74, 0x68, - 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x3a, 0x01, 0x2a, 0x42, 0x55, 0x5a, 0x4b, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, - 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, - 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x73, 0x3b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0xa2, 0xe3, 0x29, 0x04, 0x61, 0x75, - 0x74, 0x68, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x63, 0x6f, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x5f, 0x92, 0x41, + 0x29, 0x12, 0x27, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x61, 0x6e, 0x20, 0x4f, 0x49, 0x44, 0x43, 0x20, + 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2d, + 0x22, 0x22, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2d, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x3a, 0x01, 0x2a, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0xf7, 0x01, + 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x2f, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, + 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x30, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, + 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x83, 0x01, 0x92, 0x41, 0x47, 0x12, 0x45, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x20, 0x61, 0x20, 0x75, 0x73, 0x65, 0x72, 0x20, 0x74, 0x6f, 0x20, + 0x61, 0x6e, 0x20, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x72, 0x65, 0x74, + 0x72, 0x69, 0x65, 0x76, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, + 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x2e, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x33, 0x22, 0x2e, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2f, 0x7b, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, + 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x7d, 0x3a, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x3a, 0x01, 0x2a, 0x42, 0x55, 0x5a, 0x4b, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, + 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x3b, 0x73, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0xa2, 0xe3, 0x29, 0x04, 0x61, 0x75, 0x74, 0x68, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1522,7 +1607,7 @@ func file_controller_api_services_v1_auth_method_service_proto_rawDescGZIP() []b return file_controller_api_services_v1_auth_method_service_proto_rawDescData } -var file_controller_api_services_v1_auth_method_service_proto_msgTypes = make([]protoimpl.MessageInfo, 17) +var file_controller_api_services_v1_auth_method_service_proto_msgTypes = make([]protoimpl.MessageInfo, 18) var file_controller_api_services_v1_auth_method_service_proto_goTypes = []interface{}{ (*GetAuthMethodRequest)(nil), // 0: controller.api.services.v1.GetAuthMethodRequest (*GetAuthMethodResponse)(nil), // 1: controller.api.services.v1.GetAuthMethodResponse @@ -1539,59 +1624,61 @@ var file_controller_api_services_v1_auth_method_service_proto_goTypes = []interf (*ChangeStateResponse)(nil), // 12: controller.api.services.v1.ChangeStateResponse (*PasswordLoginAttributes)(nil), // 13: controller.api.services.v1.PasswordLoginAttributes (*OidcStartAttributes)(nil), // 14: controller.api.services.v1.OidcStartAttributes - (*AuthenticateRequest)(nil), // 15: controller.api.services.v1.AuthenticateRequest - (*AuthenticateResponse)(nil), // 16: controller.api.services.v1.AuthenticateResponse - (*authmethods.AuthMethod)(nil), // 17: controller.api.resources.authmethods.v1.AuthMethod - (*fieldmaskpb.FieldMask)(nil), // 18: google.protobuf.FieldMask - (*structpb.Struct)(nil), // 19: google.protobuf.Struct - (*authmethods.OidcAuthMethodAuthenticateCallbackRequest)(nil), // 20: controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateCallbackRequest - (*authmethods.OidcAuthMethodAuthenticateTokenRequest)(nil), // 21: controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateTokenRequest - (*authmethods.OidcAuthMethodAuthenticateStartResponse)(nil), // 22: controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateStartResponse - (*authmethods.OidcAuthMethodAuthenticateCallbackResponse)(nil), // 23: controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateCallbackResponse - (*authmethods.OidcAuthMethodAuthenticateTokenResponse)(nil), // 24: controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateTokenResponse - (*authtokens.AuthToken)(nil), // 25: controller.api.resources.authtokens.v1.AuthToken + (*LdapLoginAttributes)(nil), // 15: controller.api.services.v1.LdapLoginAttributes + (*AuthenticateRequest)(nil), // 16: controller.api.services.v1.AuthenticateRequest + (*AuthenticateResponse)(nil), // 17: controller.api.services.v1.AuthenticateResponse + (*authmethods.AuthMethod)(nil), // 18: controller.api.resources.authmethods.v1.AuthMethod + (*fieldmaskpb.FieldMask)(nil), // 19: google.protobuf.FieldMask + (*structpb.Struct)(nil), // 20: google.protobuf.Struct + (*authmethods.OidcAuthMethodAuthenticateCallbackRequest)(nil), // 21: controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateCallbackRequest + (*authmethods.OidcAuthMethodAuthenticateTokenRequest)(nil), // 22: controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateTokenRequest + (*authmethods.OidcAuthMethodAuthenticateStartResponse)(nil), // 23: controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateStartResponse + (*authmethods.OidcAuthMethodAuthenticateCallbackResponse)(nil), // 24: controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateCallbackResponse + (*authmethods.OidcAuthMethodAuthenticateTokenResponse)(nil), // 25: controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateTokenResponse + (*authtokens.AuthToken)(nil), // 26: controller.api.resources.authtokens.v1.AuthToken } var file_controller_api_services_v1_auth_method_service_proto_depIdxs = []int32{ - 17, // 0: controller.api.services.v1.GetAuthMethodResponse.item:type_name -> controller.api.resources.authmethods.v1.AuthMethod - 17, // 1: controller.api.services.v1.ListAuthMethodsResponse.items:type_name -> controller.api.resources.authmethods.v1.AuthMethod - 17, // 2: controller.api.services.v1.CreateAuthMethodRequest.item:type_name -> controller.api.resources.authmethods.v1.AuthMethod - 17, // 3: controller.api.services.v1.CreateAuthMethodResponse.item:type_name -> controller.api.resources.authmethods.v1.AuthMethod - 17, // 4: controller.api.services.v1.UpdateAuthMethodRequest.item:type_name -> controller.api.resources.authmethods.v1.AuthMethod - 18, // 5: controller.api.services.v1.UpdateAuthMethodRequest.update_mask:type_name -> google.protobuf.FieldMask - 17, // 6: controller.api.services.v1.UpdateAuthMethodResponse.item:type_name -> controller.api.resources.authmethods.v1.AuthMethod - 19, // 7: controller.api.services.v1.ChangeStateRequest.attributes:type_name -> google.protobuf.Struct + 18, // 0: controller.api.services.v1.GetAuthMethodResponse.item:type_name -> controller.api.resources.authmethods.v1.AuthMethod + 18, // 1: controller.api.services.v1.ListAuthMethodsResponse.items:type_name -> controller.api.resources.authmethods.v1.AuthMethod + 18, // 2: controller.api.services.v1.CreateAuthMethodRequest.item:type_name -> controller.api.resources.authmethods.v1.AuthMethod + 18, // 3: controller.api.services.v1.CreateAuthMethodResponse.item:type_name -> controller.api.resources.authmethods.v1.AuthMethod + 18, // 4: controller.api.services.v1.UpdateAuthMethodRequest.item:type_name -> controller.api.resources.authmethods.v1.AuthMethod + 19, // 5: controller.api.services.v1.UpdateAuthMethodRequest.update_mask:type_name -> google.protobuf.FieldMask + 18, // 6: controller.api.services.v1.UpdateAuthMethodResponse.item:type_name -> controller.api.resources.authmethods.v1.AuthMethod + 20, // 7: controller.api.services.v1.ChangeStateRequest.attributes:type_name -> google.protobuf.Struct 10, // 8: controller.api.services.v1.ChangeStateRequest.oidc_change_state_attributes:type_name -> controller.api.services.v1.OidcChangeStateAttributes - 17, // 9: controller.api.services.v1.ChangeStateResponse.item:type_name -> controller.api.resources.authmethods.v1.AuthMethod - 19, // 10: controller.api.services.v1.OidcStartAttributes.roundtrip_payload:type_name -> google.protobuf.Struct - 19, // 11: controller.api.services.v1.AuthenticateRequest.attributes:type_name -> google.protobuf.Struct + 18, // 9: controller.api.services.v1.ChangeStateResponse.item:type_name -> controller.api.resources.authmethods.v1.AuthMethod + 20, // 10: controller.api.services.v1.OidcStartAttributes.roundtrip_payload:type_name -> google.protobuf.Struct + 20, // 11: controller.api.services.v1.AuthenticateRequest.attributes:type_name -> google.protobuf.Struct 13, // 12: controller.api.services.v1.AuthenticateRequest.password_login_attributes:type_name -> controller.api.services.v1.PasswordLoginAttributes 14, // 13: controller.api.services.v1.AuthenticateRequest.oidc_start_attributes:type_name -> controller.api.services.v1.OidcStartAttributes - 20, // 14: controller.api.services.v1.AuthenticateRequest.oidc_auth_method_authenticate_callback_request:type_name -> controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateCallbackRequest - 21, // 15: controller.api.services.v1.AuthenticateRequest.oidc_auth_method_authenticate_token_request:type_name -> controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateTokenRequest - 19, // 16: controller.api.services.v1.AuthenticateResponse.attributes:type_name -> google.protobuf.Struct - 22, // 17: controller.api.services.v1.AuthenticateResponse.oidc_auth_method_authenticate_start_response:type_name -> controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateStartResponse - 23, // 18: controller.api.services.v1.AuthenticateResponse.oidc_auth_method_authenticate_callback_response:type_name -> controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateCallbackResponse - 24, // 19: controller.api.services.v1.AuthenticateResponse.oidc_auth_method_authenticate_token_response:type_name -> controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateTokenResponse - 25, // 20: controller.api.services.v1.AuthenticateResponse.auth_token_response:type_name -> controller.api.resources.authtokens.v1.AuthToken - 0, // 21: controller.api.services.v1.AuthMethodService.GetAuthMethod:input_type -> controller.api.services.v1.GetAuthMethodRequest - 2, // 22: controller.api.services.v1.AuthMethodService.ListAuthMethods:input_type -> controller.api.services.v1.ListAuthMethodsRequest - 4, // 23: controller.api.services.v1.AuthMethodService.CreateAuthMethod:input_type -> controller.api.services.v1.CreateAuthMethodRequest - 6, // 24: controller.api.services.v1.AuthMethodService.UpdateAuthMethod:input_type -> controller.api.services.v1.UpdateAuthMethodRequest - 8, // 25: controller.api.services.v1.AuthMethodService.DeleteAuthMethod:input_type -> controller.api.services.v1.DeleteAuthMethodRequest - 11, // 26: controller.api.services.v1.AuthMethodService.ChangeState:input_type -> controller.api.services.v1.ChangeStateRequest - 15, // 27: controller.api.services.v1.AuthMethodService.Authenticate:input_type -> controller.api.services.v1.AuthenticateRequest - 1, // 28: controller.api.services.v1.AuthMethodService.GetAuthMethod:output_type -> controller.api.services.v1.GetAuthMethodResponse - 3, // 29: controller.api.services.v1.AuthMethodService.ListAuthMethods:output_type -> controller.api.services.v1.ListAuthMethodsResponse - 5, // 30: controller.api.services.v1.AuthMethodService.CreateAuthMethod:output_type -> controller.api.services.v1.CreateAuthMethodResponse - 7, // 31: controller.api.services.v1.AuthMethodService.UpdateAuthMethod:output_type -> controller.api.services.v1.UpdateAuthMethodResponse - 9, // 32: controller.api.services.v1.AuthMethodService.DeleteAuthMethod:output_type -> controller.api.services.v1.DeleteAuthMethodResponse - 12, // 33: controller.api.services.v1.AuthMethodService.ChangeState:output_type -> controller.api.services.v1.ChangeStateResponse - 16, // 34: controller.api.services.v1.AuthMethodService.Authenticate:output_type -> controller.api.services.v1.AuthenticateResponse - 28, // [28:35] is the sub-list for method output_type - 21, // [21:28] is the sub-list for method input_type - 21, // [21:21] is the sub-list for extension type_name - 21, // [21:21] is the sub-list for extension extendee - 0, // [0:21] is the sub-list for field type_name + 21, // 14: controller.api.services.v1.AuthenticateRequest.oidc_auth_method_authenticate_callback_request:type_name -> controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateCallbackRequest + 22, // 15: controller.api.services.v1.AuthenticateRequest.oidc_auth_method_authenticate_token_request:type_name -> controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateTokenRequest + 15, // 16: controller.api.services.v1.AuthenticateRequest.ldap_login_attributes:type_name -> controller.api.services.v1.LdapLoginAttributes + 20, // 17: controller.api.services.v1.AuthenticateResponse.attributes:type_name -> google.protobuf.Struct + 23, // 18: controller.api.services.v1.AuthenticateResponse.oidc_auth_method_authenticate_start_response:type_name -> controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateStartResponse + 24, // 19: controller.api.services.v1.AuthenticateResponse.oidc_auth_method_authenticate_callback_response:type_name -> controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateCallbackResponse + 25, // 20: controller.api.services.v1.AuthenticateResponse.oidc_auth_method_authenticate_token_response:type_name -> controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateTokenResponse + 26, // 21: controller.api.services.v1.AuthenticateResponse.auth_token_response:type_name -> controller.api.resources.authtokens.v1.AuthToken + 0, // 22: controller.api.services.v1.AuthMethodService.GetAuthMethod:input_type -> controller.api.services.v1.GetAuthMethodRequest + 2, // 23: controller.api.services.v1.AuthMethodService.ListAuthMethods:input_type -> controller.api.services.v1.ListAuthMethodsRequest + 4, // 24: controller.api.services.v1.AuthMethodService.CreateAuthMethod:input_type -> controller.api.services.v1.CreateAuthMethodRequest + 6, // 25: controller.api.services.v1.AuthMethodService.UpdateAuthMethod:input_type -> controller.api.services.v1.UpdateAuthMethodRequest + 8, // 26: controller.api.services.v1.AuthMethodService.DeleteAuthMethod:input_type -> controller.api.services.v1.DeleteAuthMethodRequest + 11, // 27: controller.api.services.v1.AuthMethodService.ChangeState:input_type -> controller.api.services.v1.ChangeStateRequest + 16, // 28: controller.api.services.v1.AuthMethodService.Authenticate:input_type -> controller.api.services.v1.AuthenticateRequest + 1, // 29: controller.api.services.v1.AuthMethodService.GetAuthMethod:output_type -> controller.api.services.v1.GetAuthMethodResponse + 3, // 30: controller.api.services.v1.AuthMethodService.ListAuthMethods:output_type -> controller.api.services.v1.ListAuthMethodsResponse + 5, // 31: controller.api.services.v1.AuthMethodService.CreateAuthMethod:output_type -> controller.api.services.v1.CreateAuthMethodResponse + 7, // 32: controller.api.services.v1.AuthMethodService.UpdateAuthMethod:output_type -> controller.api.services.v1.UpdateAuthMethodResponse + 9, // 33: controller.api.services.v1.AuthMethodService.DeleteAuthMethod:output_type -> controller.api.services.v1.DeleteAuthMethodResponse + 12, // 34: controller.api.services.v1.AuthMethodService.ChangeState:output_type -> controller.api.services.v1.ChangeStateResponse + 17, // 35: controller.api.services.v1.AuthMethodService.Authenticate:output_type -> controller.api.services.v1.AuthenticateResponse + 29, // [29:36] is the sub-list for method output_type + 22, // [22:29] is the sub-list for method input_type + 22, // [22:22] is the sub-list for extension type_name + 22, // [22:22] is the sub-list for extension extendee + 0, // [0:22] is the sub-list for field type_name } func init() { file_controller_api_services_v1_auth_method_service_proto_init() } @@ -1781,7 +1868,7 @@ func file_controller_api_services_v1_auth_method_service_proto_init() { } } file_controller_api_services_v1_auth_method_service_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AuthenticateRequest); i { + switch v := v.(*LdapLoginAttributes); i { case 0: return &v.state case 1: @@ -1793,6 +1880,18 @@ func file_controller_api_services_v1_auth_method_service_proto_init() { } } file_controller_api_services_v1_auth_method_service_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AuthenticateRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_services_v1_auth_method_service_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*AuthenticateResponse); i { case 0: return &v.state @@ -1809,14 +1908,15 @@ func file_controller_api_services_v1_auth_method_service_proto_init() { (*ChangeStateRequest_Attributes)(nil), (*ChangeStateRequest_OidcChangeStateAttributes)(nil), } - file_controller_api_services_v1_auth_method_service_proto_msgTypes[15].OneofWrappers = []interface{}{ + file_controller_api_services_v1_auth_method_service_proto_msgTypes[16].OneofWrappers = []interface{}{ (*AuthenticateRequest_Attributes)(nil), (*AuthenticateRequest_PasswordLoginAttributes)(nil), (*AuthenticateRequest_OidcStartAttributes)(nil), (*AuthenticateRequest_OidcAuthMethodAuthenticateCallbackRequest)(nil), (*AuthenticateRequest_OidcAuthMethodAuthenticateTokenRequest)(nil), + (*AuthenticateRequest_LdapLoginAttributes)(nil), } - file_controller_api_services_v1_auth_method_service_proto_msgTypes[16].OneofWrappers = []interface{}{ + file_controller_api_services_v1_auth_method_service_proto_msgTypes[17].OneofWrappers = []interface{}{ (*AuthenticateResponse_Attributes)(nil), (*AuthenticateResponse_OidcAuthMethodAuthenticateStartResponse)(nil), (*AuthenticateResponse_OidcAuthMethodAuthenticateCallbackResponse)(nil), @@ -1829,7 +1929,7 @@ func file_controller_api_services_v1_auth_method_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_api_services_v1_auth_method_service_proto_rawDesc, NumEnums: 0, - NumMessages: 17, + NumMessages: 18, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/proto/controller/api/resources/accounts/v1/account.proto b/internal/proto/controller/api/resources/accounts/v1/account.proto index b454cdabc8..f46b2d02ae 100644 --- a/internal/proto/controller/api/resources/accounts/v1/account.proto +++ b/internal/proto/controller/api/resources/accounts/v1/account.proto @@ -75,6 +75,11 @@ message Account { (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.subtype) = "oidc" ]; + LdapAccountAttributes ldap_account_attributes = 103 [ + (google.api.field_visibility).restriction = "INTERNAL", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.subtype) = "ldap" + ]; } // Output only. managed_group_ids indicates IDs of the managed groups that currently contain this account @@ -128,3 +133,39 @@ message OidcAccountAttributes { // Output only. userinfo_claims are the marshaled claims from userinfo. google.protobuf.Struct userinfo_claims = 130; } + +// Attributes associated only with Accounts with type "ldap". +message LdapAccountAttributes { + // login_name of the authenticated user. This is the login_name (or username) + // entered by the user when authenticating (typically the uid or cn + // attribute). Account login names must be lower case. + string login_name = 100 [ + json_name = "login_name", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.login_name" + that: "LoginName" + } + ]; // @gotags: `class:"sensitive"` + + // Output only. full_name is a string that maps to the name attribute for the + // authenticated user. This attribute is updated every time a user + // successfully authenticates. + string full_name = 110 [json_name = "full_name"]; // @gotags: `class:"sensitive"` + + // Output only. email is a string that maps to the email address attribute for + // the authenticated user. This attribute is updated every time a user + // successfully authenticates. + string email = 120; // @gotags: `class:"sensitive"` + + // Output only. dn is the distinguished name authenticated user's entry. Will + // be null until the user's first successful authentication. This attribute + // is updated every time a user successfully authenticates. + string dn = 130; // @gotags: `class:"public"` + + // Output only. member_of_groups are the json marshalled groups the + // authenticated user is a member of. Will be null until the user's first + // successful authentication. This attribute is updated every time a user + // successfully authenticates. + repeated string member_of_groups = 140; // @gotags: `class:"public"` +} diff --git a/internal/proto/controller/api/resources/authmethods/v1/auth_method.proto b/internal/proto/controller/api/resources/authmethods/v1/auth_method.proto index 5345e5065d..c71b663059 100644 --- a/internal/proto/controller/api/resources/authmethods/v1/auth_method.proto +++ b/internal/proto/controller/api/resources/authmethods/v1/auth_method.proto @@ -73,6 +73,11 @@ message AuthMethod { (custom_options.v1.subtype) = "oidc", (google.api.field_visibility).restriction = "INTERNAL" ]; + LdapAuthMethodAttributes ldap_auth_methods_attributes = 103 [ + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.subtype) = "ldap", + (google.api.field_visibility).restriction = "INTERNAL" + ]; } // Output only. Whether this auth method is the primary auth method for it's scope. @@ -303,3 +308,266 @@ message OidcAuthMethodAuthenticateTokenResponse { // the consumer. string status = 10; // @gotags: `class:"public"` } + +// The attributes of an LDAP typed auth method. +message LdapAuthMethodAttributes { + // Output only. The state of the auth method. Will be "inactive", + // "active-private", or "active-public". + string state = 10 [ + json_name = "state", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.state" + that: "OperationalState" + } + ]; // @gotags: `class:"public"` + + // start_tls if true, issues a StartTLS command after establishing an + // unencrypted connection. Defaults to false. + bool start_tls = 20 [ + json_name = "start_tls", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.start_tls" + that: "StartTls" + } + ]; // @gotags: `class:"public"` + + // insecure_tls if true, skips LDAP server SSL certificate validation - + // insecure and use with caution. Defaults to false. + bool insecure_tls = 30 [ + json_name = "insecure_tls", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.insecure_tls" + that: "InsecureTls" + } + ]; // @gotags: `class:"public"` + + // discover_dn if true, use anon bind to discover the bind DN of a user. + // Defaults to false. + bool discover_dn = 40 [ + json_name = "discover_dn", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.discover_dn" + that: "DiscoverDn" + } + ]; // @gotags: `class:"public"` + + // anon_group_search if true, use anon bind when performing LDAP group + // searches. Defaults to false. + bool anon_group_search = 50 [ + json_name = "anon_group_search", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.anon_group_search" + that: "AnonGroupSearch" + } + ]; // @gotags: `class:"public"` + + // upn_domain is the userPrincipalDomain used to construct the UPN string for + // the authenticating user. The constructed UPN will appear as + // [username]@UPNDomain Example: example.com, which will cause Boundary to + // bind as username@example.com when authenticating the user. + google.protobuf.StringValue upn_domain = 60 [ + json_name = "upn_domain", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.upn_domain" + that: "UpnDomain" + } + ]; // @gotags: `class:"public"` + + // urls are the LDAP URLS that specify LDAP servers to connection to. There + // must be at lease on URL for each LDAP auth method. When attempting to + // connect, the URLs are tried in the order specified. These are Value Objects + // that will be stored as Url messages, and are operated on as a complete set + // (not individually). + repeated string urls = 70 [ + json_name = "urls", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.urls" + that: "Urls" + } + ]; // @gotags: `class:"public"` + + // user_dn (optional) is the base DN under which to perform user search. + // Example: ou=Users,dc=example,dc=com + google.protobuf.StringValue user_dn = 80 [ + json_name = "user_dn", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.user_dn" + that: "UserDn" + } + ]; // @gotags: `class:"public"` + + // user_attr (optional) is the attribute on user attribute entry matching the + // username passed when authenticating. Examples: cn, uid + google.protobuf.StringValue user_attr = 90 [ + json_name = "user_attr", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.user_attr" + that: "UserAttr" + } + ]; // @gotags: `class:"public"` + + // user_filter (optional) is a go template used to construct a LDAP user + // search filter. The template can access the following context variables: + // [UserAttr, Username]. The default userfilter is + // ({{.UserAttr}}={{.Username}}) or + // (userPrincipalName={{.Username}}@UPNDomain) if the upndomain parameter is + // set. + google.protobuf.StringValue user_filter = 100 [ + json_name = "user_filter", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.user_filter", + that: "UserFilter" + } + ]; // @gotags: `class:"public"` + + // enable_groups if true, an authenticated user's groups will be found during + // authentication. Defaults to false. + bool enable_groups = 110 [ + json_name = "enable_groups", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.enable_groups" + that: "EnableGroups" + } + ]; // @gotags: `class:"public"` + + // group_dn (optional) is the base DN under which to perform user search. + // Example: ou=Groups,dc=example,dc=com + // + // Note: there is no default, so no base dn will be used for group searches if + // it's not specified. + google.protobuf.StringValue group_dn = 120 [ + json_name = "group_dn", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.group_dn" + that: "GroupDn" + } + ]; // @gotags: `class:"public"` + + // group_attr (optional) is the LDAP attribute to follow on objects returned + // by GroupFilter in order to enumerate user group membership. Examples: for + // GroupFilter queries returning group objects, use: cn. For queries returning + // user objects, use: memberOf. The default is cn. + google.protobuf.StringValue group_attr = 130 [ + json_name = "group_attr", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.group_attr" + that: "GroupAttr" + } + ]; // @gotags: `class:"public"` + + // group_filter (optional) is a Go template used when constructing the group + // membership query. The template can access the following context variables: + // [UserDN, Username]. The default is + // (|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}})), + // which is compatible with several common directory schemas. + google.protobuf.StringValue group_filter = 140 [ + json_name = "group_filter", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.group_filter", + that: "GroupFilter" + } + ]; // @gotags: `class:"public"` + + // certificates are optional PEM encoded x509 certificates in ASN.1 DER form + // that can be used as trust anchors when connecting to an LDAP provider. + // These are Value Objects that will be stored as Certificate messages, and + // are operatated on as a complete set (not individually). + repeated string certificates = 150 [ + json_name = "certificates", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.certificates" + that: "Certificates" + } + ]; // @gotags: `class:"public"` + + // client_certificate is the optional certificate encoded as PEM. It must be + // set if an optional client_certificate_key specified + google.protobuf.StringValue client_certificate = 160 [ + json_name = "client_certificate", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.client_certificate", + that: "ClientCertificate" + } + ]; // @gotags: `class:"public"` + + // Input only. The client_certificate_key (optional) is the plain-text of the + // certificate key data encoded as PEM. + google.protobuf.StringValue client_certificate_key = 170 [ + json_name = "client_certificate_key", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.client_certificate_key", + that: "ClientCertificateKey" + } + ]; // @gotags: `class:"secret"` + + // Output only. The HMAC'd value of the client certificate key to indicate + // whether the certificate key has changed. + string client_certificate_key_hmac = 180 [json_name = "client_certificate_key_hmac"]; // @gotags: `class:"public"` + + // bind_dn (optional) is the distinguished name of entry to bind when + // performing user and group search. Example: + // cn=vault,ou=Users,dc=example,dc=com + google.protobuf.StringValue bind_dn = 190 [ + json_name = "bind_dn", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.bind_dn", + that: "BindDn" + } + ]; // @gotags: `class:"public"` + + // Input only. The bind_password (optional) is the password to use along with + // binddn when performing user search. + google.protobuf.StringValue bind_password = 200 [ + json_name = "bind_password", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.bind_password", + that: "BindPassword" + } + ]; // @gotags: `class:"secret"` + + // Output only. The HMAC'd value of the bind password to indicate + // whether the password has changed. + string bind_password_hmac = 210 [json_name = "bind_password_hmac"]; // @gotags: `class:"public"` + + bool use_token_groups = 220 [ + json_name = "use_token_groups", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.use_token_groups" + that: "UseTokenGroups" + } + ]; // @gotags: `class:"public"` + + // account_attribute_maps are optional attribute maps from custom attributes + // to the standard attributes of fullname and email. These maps are + // represented as key=value where the key equals the from_attribute and the + // value equals the to_attribute. For example "preferredName=fullName". All + // attribute names are case insensitive. + repeated string account_attribute_maps = 230 [ + json_name = "account_attribute_maps", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.account_attribute_maps" + that: "AccountAttributeMaps" + } + ]; // @gotags: `class:"public"` +} diff --git a/internal/proto/controller/api/resources/managedgroups/v1/managed_group.proto b/internal/proto/controller/api/resources/managedgroups/v1/managed_group.proto index f67bca0d20..439e2c8913 100644 --- a/internal/proto/controller/api/resources/managedgroups/v1/managed_group.proto +++ b/internal/proto/controller/api/resources/managedgroups/v1/managed_group.proto @@ -70,6 +70,11 @@ message ManagedGroup { (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.subtype) = "oidc" ]; + LdapManagedGroupAttributes ldap_managed_group_attributes = 102 [ + (google.api.field_visibility).restriction = "INTERNAL", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.subtype) = "ldap" + ]; } // Output only. The IDs of the current set of members (accounts) that are associated with this ManagedGroup. @@ -91,3 +96,16 @@ message OidcManagedGroupAttributes { } ]; // @gotags: `class:"public"` } + +// Attributes associated only with ManagedGroups with type "ldap". +message LdapManagedGroupAttributes { + // The list of groups that make up the ManagedGroup + repeated string group_names = 100 [ + json_name = "group_names", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.group_names" + that: "GroupNames" + } + ]; // @gotags: `class:"public"` +} diff --git a/internal/proto/controller/api/services/v1/auth_method_service.proto b/internal/proto/controller/api/services/v1/auth_method_service.proto index 9aa9613f8f..8ad4da7935 100644 --- a/internal/proto/controller/api/services/v1/auth_method_service.proto +++ b/internal/proto/controller/api/services/v1/auth_method_service.proto @@ -202,6 +202,14 @@ message OidcStartAttributes { string cached_roundtrip_payload = 2; // @gotags: `class:"sensitive"` } +// The layout of the struct for "attributes" field in AuthenticateRequest for an +// ldap type. This message isn't directly referenced anywhere but is used here +// to define the expected field names and types. +message LdapLoginAttributes { + string login_name = 10 [json_name = "login_name"]; // @gotags: `class:"sensitive"` + string password = 20; // @gotags: `class:"secret"` +} + message AuthenticateRequest { // The ID of the Auth Method in the system that should be used for authentication. string auth_method_id = 1 [json_name = "auth_method_id"]; // @gotags: `class:"public"` @@ -223,6 +231,7 @@ message AuthenticateRequest { OidcStartAttributes oidc_start_attributes = 8 [(google.api.field_visibility).restriction = "INTERNAL"]; controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateCallbackRequest oidc_auth_method_authenticate_callback_request = 9 [(google.api.field_visibility).restriction = "INTERNAL"]; controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateTokenRequest oidc_auth_method_authenticate_token_request = 10 [(google.api.field_visibility).restriction = "INTERNAL"]; + LdapLoginAttributes ldap_login_attributes = 11 [(google.api.field_visibility).restriction = "INTERNAL"]; } // The command to perform. string command = 5 [json_name = "command"]; // @gotags: `class:"public"` diff --git a/internal/proto/controller/storage/auth/ldap/store/v1/ldap.proto b/internal/proto/controller/storage/auth/ldap/store/v1/ldap.proto new file mode 100644 index 0000000000..034c57b83e --- /dev/null +++ b/internal/proto/controller/storage/auth/ldap/store/v1/ldap.proto @@ -0,0 +1,592 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +syntax = "proto3"; + +// Package store provides protobufs for storing types in the ldap package. +package controller.storage.auth.ldap.store.v1; + +import "controller/custom_options/v1/options.proto"; +import "controller/storage/timestamp/v1/timestamp.proto"; + +option go_package = "github.com/hashicorp/boundary/internal/auth/ldap/store;store"; + +// AuthMethod represents an LDAP auth method. +message AuthMethod { + // public_id is the PK and is the external public identifier of the auth + // method. + // @inject_tag: `gorm:"primary_key"` + string public_id = 10; + + // create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 20; + + // update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp update_time = 30; + + // name is optional. If set, it must be unique within scope_id. + // @inject_tag: `gorm:"default:null"` + string name = 40 [(custom_options.v1.mask_mapping) = { + this: "Name" + that: "name" + }]; + + // description is optional. + // @inject_tag: `gorm:"default:null"` + string description = 50 [(custom_options.v1.mask_mapping) = { + this: "Description" + that: "description" + }]; + + // The scope_id of the owning scope. Must be set. + // @inject_tag: `gorm:"not_null"` + string scope_id = 60; + + // @inject_tag: `gorm:"default:null"` + uint32 version = 70; + + // operational_state is the current state of the auth_ldap_method (inactive, + // active-private, or active-public). + // @inject_tag: `gorm:"column:state;not_null"` + string operational_state = 80 [(custom_options.v1.mask_mapping) = { + this: "OperationalState" + that: "attributes.state" + }]; + + // start_tls if true, issues a StartTLS command after establishing an + // unencrypted connection. Defaults to false. + // @inject_tag: `gorm:"not_null"` + bool start_tls = 90 [(custom_options.v1.mask_mapping) = { + this: "StartTls" + that: "attributes.start_tls" + }]; + + // insecure_tls if true, skips LDAP server SSL certificate validation - + // insecure and use with caution. Defaults to false. + // @inject_tag: `gorm:"not_null;default:false"` + bool insecure_tls = 100 [(custom_options.v1.mask_mapping) = { + this: "InsecureTls" + that: "attributes.insecure_tls" + }]; + + // discover_dn if true, use anon bind to discover the bind DN of a user. + // Defaults to false. + // @inject_tag: `gorm:"not_null;default:false"` + bool discover_dn = 110 [(custom_options.v1.mask_mapping) = { + this: "DiscoverDn" + that: "attributes.discover_dn" + }]; + + // anon_group_search if true, use anon bind when performing LDAP group + // searches. Defaults to false. + // @inject_tag: `gorm:"not_null;default:false"` + bool anon_group_search = 120 [(custom_options.v1.mask_mapping) = { + this: "AnonGroupSearch" + that: "attributes.anon_group_search" + }]; + + // upn_domain is the userPrincipalDomain used to construct the UPN string for + // the authenticating user. The constructed UPN will appear as + // [username]@UPNDomain Example: example.com, which will cause Boundary to + // bind as username@example.com when authenticating the user. + // @inject_tag: `gorm:"default:null"` + string upn_domain = 130 [(custom_options.v1.mask_mapping) = { + this: "UpnDomain" + that: "attributes.upn_domain" + }]; + + // urls are the LDAP URLS that specify LDAP servers to connection to. There + // must be at lease on URL for each LDAP auth method. When attempting to + // connect, the URLs are tried in the order specified. These are Value Objects + // that will be stored as Url messages, and are operated on as a complete set + // (not individually). + // @inject_tag: `gorm:"-"` + repeated string urls = 140 [(custom_options.v1.mask_mapping) = { + this: "Urls" + that: "attributes.urls" + }]; + + // user_dn (optional) is the base DN under which to perform user search. + // Example: ou=Users,dc=example,dc=com + // @inject_tag: `gorm:"-"` + string user_dn = 150 [(custom_options.v1.mask_mapping) = { + this: "UserDn" + that: "attributes.user_dn" + }]; + + // user_attr (optional) is the attribute on user's entry matching the username + // passed when authenticating. Examples: cn, uid + // @inject_tag: `gorm:"-"` + string user_attr = 160 [(custom_options.v1.mask_mapping) = { + this: "UserAttr" + that: "attributes.user_attr" + }]; + + // user_filter (optional) is a go template used to construct a LDAP user + // search filter. The template can access the following context variables: + // [UserAttr, Username]. The default userfilter is + // ({{.UserAttr}}={{.Username}}) or + // (userPrincipalName={{.Username}}@UPNDomain) if the upndomain parameter is + // set. + // @inject_tag: `gorm:"-"` + string user_filter = 170 [(custom_options.v1.mask_mapping) = { + this: "UserFilter" + that: "attributes.user_filter" + }]; + + // enable_groups if true, an authenticated user's groups will be found during + // authentication. Defaults to false. + // @inject_tag: `gorm:"not_null;default:false"` + bool enable_groups = 175 [(custom_options.v1.mask_mapping) = { + this: "EnableGroups" + that: "attributes.enable_groups" + }]; + + // group_dn (optional) is the base DN under which to perform group search. + // Example: ou=Groups,dc=example,dc=com + // + // Note: there is no default, so no base dn will be used for group searches if + // it's not specified. + // @inject_tag: `gorm:"-"` + string group_dn = 180 [(custom_options.v1.mask_mapping) = { + this: "GroupDn" + that: "attributes.group_dn" + }]; + + // group_attr (optional) is the LDAP attribute to follow on objects returned + // by GroupFilter in order to enumerate user group membership. Examples: for + // GroupFilter queries returning group objects, use: cn. For queries returning + // user objects, use: memberOf. The default is cn. + // @inject_tag: `gorm:"-"` + string group_attr = 190 [(custom_options.v1.mask_mapping) = { + this: "GroupAttr" + that: "attributes.group_attr" + }]; + + // group_filter (optional) is a Go template used when constructing the group + // membership query. The template can access the following context variables: + // [UserDN, Username]. The default is + // (|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}})), + // which is compatible with several common directory schemas. + // @inject_tag: `gorm:"-"` + string group_filter = 200 [(custom_options.v1.mask_mapping) = { + this: "GroupFilter" + that: "attributes.group_filter" + }]; + + // certificates are optional PEM encoded x509 certificates in ASN.1 DER form + // that can be used as trust anchors when connecting to an LDAP provider. + // These are Value Objects that will be stored as Certificate messages, and + // are operated on as a complete set (not individually). + // @inject_tag: `gorm:"-"` + repeated string certificates = 210 [(custom_options.v1.mask_mapping) = { + this: "Certificates" + that: "attributes.certificates" + }]; + + // client_certificate is the certificate in ASN.1 DER form encoded as PEM. It + // must be set. + // @inject_tag: `gorm:"-"` + string client_certificate = 220 [(custom_options.v1.mask_mapping) = { + this: "ClientCertificate" + that: "attributes.client_certificate" + }]; + + // client_certificate_key (optional) is the plain-text of the certificate key + // data in PKCS #8, ASN.1 DER form. We are not storing this plain-text key in + // the database. + // @inject_tag: `gorm:"-"` + bytes client_certificate_key = 230 [(custom_options.v1.mask_mapping) = { + this: "ClientCertificateKey" + that: "attributes.client_certificate_key" + }]; + + // client_certificate_key_hmac is a sha256-hmac of the unencrypted + // client_certificate_key_hmac that is returned from the API for read. It is + // recalculated everytime the raw client_certificate_key_hmac is updated in + // the database. + // @inject_tag: `gorm:"-"` + bytes client_certificate_key_hmac = 240; + + // bind_dn (optional) is the distinguished name of entry to bind when + // performing user and group search. Example: + // cn=vault,ou=Users,dc=example,dc=com + // @inject_tag: `gorm:"-"` + string bind_dn = 250 [(custom_options.v1.mask_mapping) = { + this: "BindDn" + that: "attributes.bind_dn" + }]; + + // bind_password (optional) is the password to use along with binddn when + // performing user search. (This plaintext is not stored in the database) + // @inject_tag: `gorm:"-"` + string bind_password = 260 [(custom_options.v1.mask_mapping) = { + this: "BindPassword" + that: "attributes.bind_password" + }]; + + // bind_password_hmac is a sha256-hmac of the unencrypted bind_password that + // is returned from the API for read. It is recalculated everytime the raw + // password is updated in the database. + // @inject_tag: `gorm:"-"` + bytes bind_password_hmac = 270; + + // is_primary_auth_method is a read-only output field which indicates if the + // auth method is set as the scope's primary auth method. + // @inject_tag: `gorm:"-"` + bool is_primary_auth_method = 280; + + // use_token_groups if true, use the Active Directory tokenGroups constructed + // attribute of the user to find the group memberships. This will find all + // security groups including nested ones. + // @inject_tag: `gorm:"not_null;default:false"` + bool use_token_groups = 290 [(custom_options.v1.mask_mapping) = { + this: "UseTokenGroups" + that: "attributes.use_token_groups" + }]; + + // account_attribute_maps are optional attribute maps from custom attributes + // to the standard attributes of fullname and email. These maps are + // represented as key=value where the key equals the from_attribute and the + // value equals the to_attribute. For example "preferredName=fullName". All + // attribute names are case insensitive. + // @inject_tag: `gorm:"-"` + repeated string account_attribute_maps = 300 [(custom_options.v1.mask_mapping) = { + this: "AccountAttributeMaps" + that: "attributes.account_attribute_maps" + }]; +} + +// Url represents LDAP URLs that specify LDAP servers to connection to. There +// must be at lease on URL for each LDAP auth method. +message Url { + // create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 10; + + // ldap_method_id is the FK to the URL's LDAP auth method. + // @inject_tag: `gorm:"primary_key"` + string ldap_method_id = 20; + + // connection_priority represents the priority (aka order) of the url in the + // list of ldap urls for the auth method. + // @inject_tag: `gorm:"primary_key"` + uint32 connection_priority = 30; + + // server_url is the LDAP server URL. The URL scheme must be either ldap or ldaps. + // The port is optional.If no port is specified, then a default of 389 is used + // for ldap and a default of 689 is used for ldaps. (see rfc4516 for more + // information about LDAP URLs) + // @inject_tag: `gorm:"column:url;not_null"` + string server_url = 40; +} + +// UserEntrySearchConf represent a set of optional configuration fields used to +// search for user entries. +message UserEntrySearchConf { + // create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 10; + + // ldap_method_id is the FK to the UserEntrySearchConf's LDAP auth method. + // @inject_tag: `gorm:"primary_key"` + string ldap_method_id = 20; + + // user_dn is the base DN under which to perform user search. Example: + // ou=Users,dc=example,dc=com + // @inject_tag: `gorm:"default:null"` + string user_dn = 30; + + // user_attr is the attribute on user attribute entry matching the username + // passed when authenticating. Examples: cn, uid + // @inject_tag: `gorm:"default:null"` + string user_attr = 40; + + // user_filter is a go template used to construct a LDAP user search filter. + // The template can access the following context variables: [UserAttr, + // Username]. The default userfilter is ({{.UserAttr}}={{.Username}}) or + // (userPrincipalName={{.Username}}@UPNDomain) if the upndomain parameter is + // set. + // @inject_tag: `gorm:"default:null"` + string user_filter = 50; +} + +// GroupEntrySearchConf represent a set of optional configuration fields used to +// search for group entries. +message GroupEntrySearchConf { + // create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 10; + + // ldap_method_id is the FK to the GroupEntrySearchConf's LDAP auth method. + // @inject_tag: `gorm:"primary_key"` + string ldap_method_id = 20; + + // group_dn is the base DN under which to perform user search. Example: + // ou=Groups,dc=example,dc=com + // @inject_tag: `gorm:"default:null"` + string group_dn = 30; + + // group_attr is the LDAP attribute to follow on objects returned by + // GroupFilter in order to enumerate user group membership. Examples: for + // GroupFilter queries returning group objects, use: cn. For queries returning + // user objects, use: memberOf. The default is cn. + // @inject_tag: `gorm:"default:null"` + string group_attr = 40; + + // user_filter is a Go template used when constructing the group membership + // query. The template can access the following context variables: [UserDN, + // Username]. The default is + // (|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}})), + // which is compatible with several common directory schemas. + // @inject_tag: `gorm:"default:null"` + string group_filter = 50; +} + +// Certificate entries are optional PEM encoded x509 certificates. Each entry is +// a single certificate. An ldap auth method may have 0 or more of these +// optional x509s. If an auth method has any cert entries, they are used as +// trust anchors when connecting to the auth method's ldap provider (instead of +// the host system's cert chain). +message Certificate { + // create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 10; + + // ldap_method_id is the FK to the Certificate's LDAP auth method. + // @inject_tag: `gorm:"primary_key"` + string ldap_method_id = 20; + + // certificate is a PEM encoded x509 in ASN.1 DER form. + // @inject_tag: `gorm:"column:certificate;primary_key"` + string cert = 30; +} + +// ClientCertificate represent a set of optional configuration fields used for +// specifying a mTLS client cert for LDAP connections. +message ClientCertificate { + // create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 10; + + // ldap_method_id is the FK to the ClientCertificate's LDAP auth method. + // @inject_tag: `gorm:"primary_key"` + string ldap_method_id = 20; + + // certificate is the PEM encoded certificate in ASN.1 DER. + // It must be set. + // @inject_tag: `gorm:"not_null"` + bytes certificate = 30; + + // certificate_key is the plain-text of the certificate key data in PKCS #8, + // ASN.1 DER form. We are not storing this plain-text key in the database. + // @inject_tag: `gorm:"-" wrapping:"pt,certificate_key_data"` + bytes certificate_key = 40; + + // ct_certificate_key is the ciphertext of the certificate key data. It + // is stored in the database. + // @inject_tag: `gorm:"column:certificate_key;not_null" wrapping:"ct,certificate_key_data"` + bytes ct_certificate_key = 50; + + // certificate_key_hmac is a sha256-hmac of the unencrypted certificate_key that + // is returned from the API for read. It is recalculated everytime the raw + // certificate_key is updated. + // @inject_tag: `gorm:"not_null"` + bytes certificate_key_hmac = 60; + + // The key_id of the kms database key used for encrypting this entry. + // It must be set. + // @inject_tag: `gorm:"not_null"` + string key_id = 70; +} + +// BindCredentail (optional) represent parameters which allow Boundary to bind +// (aka authenticate) using the credentials provided when searching for the user +// entry used to authenticate the end user. +message BindCredential { + // create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 10; + + // ldap_method_id is the FK to the BindCredential's LDAP auth method. + // @inject_tag: `gorm:"primary_key"` + string ldap_method_id = 20; + + // dn is the distinguished name of the entry to bind when performing + // user and group search. Example: cn=vault,ou=Users,dc=example,dc=com + // @inject_tag: `gorm:"not_null"` + string dn = 30; + + // password is the plain-text password to use along with dn. We are not + // storing this plain-text key in the database. + // @inject_tag: `gorm:"-" wrapping:"pt,password_data"` + bytes password = 40; + + // ct_password_key is the ciphertext of the password. It is stored in the database. + // @inject_tag: `gorm:"column:password;not_null" wrapping:"ct,password_data"` + bytes ct_password = 50; + + // password_hmac is a sha256-hmac of the unencrypted password that is returned + // from the API for read. It is recalculated everytime the raw password is + // updated. + // @inject_tag: `gorm:"not_null"` + bytes password_hmac = 60; + + // The key_id of the kms database key used for encrypting this entry. + // It must be set. + // @inject_tag: `gorm:"not_null"` + string key_id = 70; +} + +// Account respresent Accounts associated with an LDAP auth method. +message Account { + // public_id is the PK and is the external public identifier of the account + // @inject_tag: `gorm:"primary_key"` + string public_id = 10; + + // create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 20; + + // update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp update_time = 30; + + // auth_method_id is the FK to the Account's LDAP auth method. + // @inject_tag: `gorm:"not_null"` + string auth_method_id = 40; + + // name is optional. If set, it must be unique within scope_id. + // @inject_tag: `gorm:"default:null"` + string name = 50 [(custom_options.v1.mask_mapping) = { + this: "Name" + that: "name" + }]; + + // description is optional. + // @inject_tag: `gorm:"default:null"` + string description = 60 [(custom_options.v1.mask_mapping) = { + this: "Description" + that: "description" + }]; + + // The scope_id of the owning scope. Must be set. The scope_id column is not + // included here as it is used only to ensure data integrity in the database + // between iam users and auth methods. + // @inject_tag: `gorm:"not_null"` + string scope_id = 70; + + // @inject_tag: `gorm:"default:null"` + uint32 version = 80; + + // login_name of the authenticated user. This is the login_name (or username) + // entered by the user when authenticating (typically the uid or cn + // attribute). Account login names must be lower case. + // @inject_tag: `gorm:"not_null"` + string login_name = 90; + + // full_name is a string that maps to the name attribute for the authenticated + // user. This attribute is updated every time a user successfully + // authenticates. + // @inject_tag: `gorm:"default:null"` + string full_name = 100; + + // email is a string that maps to the email address attribute for the + // authenticated user. This attribute is updated every time a user + // successfully authenticates. + // @inject_tag: `gorm:"default:null"` + string email = 110; + + // dn is the distinguished name authenticated user's entry. Will be null until + // the user's first successful authentication. This attribute is updated + // every time a user successfully authenticates. + // @inject_tag: `gorm:"default:null"` + string dn = 120; + + // member_of_groups are the json marshalled groups the authenticated user is a + // member of. Will be null until the user's first successful authentication. + // This attribute is updated every time a user successfully authenticates. + // @inject_tag: `gorm:"default:null"` + string member_of_groups = 140; +} + +// AccountAttributeMap entries are optional from/to account attribute maps. +message AccountAttributeMap { + // @inject_tag: `gorm:"primary_key"` + string ldap_method_id = 10; + + // from_attribute is the attribute from the user's entry that you need to map + // to a standard account attribute. + // @inject_tag: `gorm:"not_null"` + string from_attribute = 20; + + // to_attribute is the standard account attribute to map the from_attribute + // to. Valid values are: fullname, email + // @inject_tag: `gorm:"column:to_attribute;primary_key"` + string to_attribute = 30; + + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 40; +} + +// ManagedGroup entries provide an LDAP auth method implementation of managed +// groups. +message ManagedGroup { + // @inject_tag: `gorm:"primary_key"` + string public_id = 10; + + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 20; + + // The update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp update_time = 30; + + // name is optional. If set, it must be unique within auth_method_id. + // @inject_tag: `gorm:"default:null"` + string name = 40 [(custom_options.v1.mask_mapping) = { + this: "Name" + that: "name" + }]; + + // description is optional. + // @inject_tag: `gorm:"default:null"` + string description = 50 [(custom_options.v1.mask_mapping) = { + this: "Description" + that: "description" + }]; + + // @inject_tag: `gorm:"default:null"` + uint32 version = 60; + + // auth_method_id is the fk to the account's auth method. + // @inject_tag: `gorm:"not_null"` + string auth_method_id = 70; + + // groups is json marshalled list of groups that make up the ManagedGroup + // @inject_tag: `gorm:"not_null"` + string group_names = 80 [(custom_options.v1.mask_mapping) = { + this: "GroupNames" + that: "attributes.group_names" + }]; +} + +// ManagedGroupMemberAccount contains a mapping between a managed group and a +// member account. +message ManagedGroupMemberAccount { + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 10; + + // managed_group_id is the fk to the oidc managed group public id + // @inject_tag: `gorm:"primary_key"` + string managed_group_id = 20; + + // member_id is the fk to the oidc account public id + // @inject_tag: `gorm:"primary_key"` + string member_id = 30; +} diff --git a/internal/tests/api/accounts/account_test.go b/internal/tests/api/accounts/account_test.go index 53ea92410a..bb154f0d35 100644 --- a/internal/tests/api/accounts/account_test.go +++ b/internal/tests/api/accounts/account_test.go @@ -142,6 +142,60 @@ func comparableSlice(in []*accounts.Account) []accounts.Account { return filtered } +func TestListLdap(t *testing.T) { + assert, require := assert.New(t), require.New(t) + tc := controller.NewTestController(t, nil) + defer tc.Shutdown() + + client := tc.Client() + require.NotNil(client) + token := tc.Token() + require.NotNil(token) + client.SetToken(token.Token) + org := iam.TestOrg(t, tc.IamRepo(), iam.WithUserId(token.UserId)) + amClient := authmethods.NewClient(client) + + amResult, err := amClient.Create(tc.Context(), "ldap", org.PublicId, + authmethods.WithName("foo"), + authmethods.WithLdapAuthMethodUrls([]string{"ldaps://ldap1"})) + require.NoError(err) + require.NotNil(amResult) + am := amResult.Item + + accountClient := accounts.NewClient(client) + + lr, err := accountClient.List(tc.Context(), am.Id) + require.NoError(err) + expected := lr.Items + assert.Len(expected, 0) + + cr, err := accountClient.Create(tc.Context(), am.Id, + accounts.WithLdapAccountLoginName("login-name0")) + require.NoError(err) + expected = append(expected, cr.Item) + + ulResult, err := accountClient.List(tc.Context(), am.Id) + require.NoError(err) + assert.ElementsMatch(comparableSlice(expected[:1]), comparableSlice(ulResult.Items)) + + for i := 1; i < 10; i++ { + newAcctResult, err := accountClient.Create(tc.Context(), am.Id, + accounts.WithLdapAccountLoginName(fmt.Sprintf("login-name-%d", i))) + require.NoError(err) + expected = append(expected, newAcctResult.Item) + } + ulResult, err = accountClient.List(tc.Context(), am.Id) + require.NoError(err) + assert.ElementsMatch(comparableSlice(expected), comparableSlice(ulResult.Items)) + + filterItem := expected[3] + ulResult, err = accountClient.List(tc.Context(), am.Id, + accounts.WithFilter(fmt.Sprintf(`"/item/attributes/login_name"==%q`, filterItem.Attributes["login_name"]))) + require.NoError(err) + require.Len(ulResult.Items, 1) + assert.Equal(filterItem.Id, ulResult.Items[0].Id) +} + func TestCrudPassword(t *testing.T) { assert, require := assert.New(t), require.New(t) tc := controller.NewTestController(t, nil) @@ -229,6 +283,61 @@ func TestCrudOidc(t *testing.T) { require.NoError(err) } +func TestCrudLdap(t *testing.T) { + assert, require := assert.New(t), require.New(t) + tc := controller.NewTestController(t, nil) + defer tc.Shutdown() + + client := tc.Client() + token := tc.Token() + client.SetToken(token.Token) + amClient := authmethods.NewClient(client) + amResult, err := amClient.Create(tc.Context(), "ldap", "global", + authmethods.WithName("foo"), + authmethods.WithLdapAuthMethodUrls([]string{"ldaps://ldap1"})) + + require.NoError(err) + require.NotNil(amResult) + amId := amResult.Item.Id + + accountClient := accounts.NewClient(client) + + checkAccount := func(step string, u *accounts.Account, err error, wantedName string, wantedVersion uint32) { + assert.NoError(err, step) + require.NotNil(u, "returned no resource", step) + gotName := "" + if u.Name != "" { + gotName = u.Name + } + assert.Equal(wantedName, gotName, step) + assert.EqualValues(wantedVersion, u.Version) + } + + u, err := accountClient.Create(tc.Context(), amId, accounts.WithName("foo"), + accounts.WithLdapAccountLoginName("login-name")) + require.NoError(err) + require.NotEmpty(u) + checkAccount("create", u.Item, err, "foo", 1) + + u, err = accountClient.Read(tc.Context(), u.Item.Id) + require.NoError(err) + require.NotEmpty(u) + checkAccount("read", u.Item, err, "foo", 1) + + u, err = accountClient.Update(tc.Context(), u.Item.Id, u.Item.Version, accounts.WithName("bar")) + require.NoError(err) + require.NotEmpty(u) + checkAccount("update", u.Item, err, "bar", 2) + + u, err = accountClient.Update(tc.Context(), u.Item.Id, u.Item.Version, accounts.DefaultName()) + require.NoError(err) + require.NotEmpty(u) + checkAccount("update", u.Item, err, "", 3) + + _, err = accountClient.Delete(tc.Context(), u.Item.Id) + require.NoError(err) +} + func TestCustomMethods(t *testing.T) { assert, require := assert.New(t), require.New(t) tc := controller.NewTestController(t, nil) diff --git a/internal/tests/api/authmethods/authmethod_test.go b/internal/tests/api/authmethods/authmethod_test.go index a487826f44..2fd3d2648e 100644 --- a/internal/tests/api/authmethods/authmethod_test.go +++ b/internal/tests/api/authmethods/authmethod_test.go @@ -147,6 +147,43 @@ func TestCrud(t *testing.T) { oidc, err = amClient.Update(tc.Context(), oidc.Item.Id, oidc.Item.Version, authmethods.DefaultName()) require.NoError(err) checkAuthMethod("update", oidc.Item, "", 3) + + _ = os.WriteFile(eventConfig.AuditEvents.Name(), nil, 0o666) // clean out audit events from previous calls + // ldap auth methods + ldap, err := amClient.Create(tc.Context(), "ldap", global, + authmethods.WithName("ldap-foo"), + authmethods.WithLdapAuthMethodUrls([]string{"ldaps://ldap1"}), + authmethods.WithLdapAuthMethodBindDn("bind-dn"), + authmethods.WithLdapAuthMethodBindPassword("pass")) + require.NoError(err) + t.Cleanup(func() { + _, err = amClient.Delete(tc.Context(), ldap.Item.Id) + require.NoError(err) + }) + checkAuthMethod("create", ldap.Item, "ldap-foo", 1) + got = tests_api.CloudEventFromFile(t, eventConfig.AuditEvents.Name()) + + reqItem = tests_api.GetEventDetails(t, got, "request")["item"].(map[string]any) + tests_api.AssertRedactedValues(t, reqItem) + tests_api.AssertRedactedValues(t, reqItem["Attrs"]) + tests_api.AssertRedactedValues(t, reqItem["Attrs"].(map[string]any)["LdapAuthMethodsAttributes"]) + + respItem = tests_api.GetEventDetails(t, got, "response")["item"].(map[string]any) + tests_api.AssertRedactedValues(t, respItem) + tests_api.AssertRedactedValues(t, respItem["Attrs"]) + tests_api.AssertRedactedValues(t, respItem["Attrs"].(map[string]any)["LdapAuthMethodsAttributes"]) + + ldap, err = amClient.Read(tc.Context(), ldap.Item.Id) + require.NoError(err) + checkAuthMethod("read", ldap.Item, "ldap-foo", 1) + + ldap, err = amClient.Update(tc.Context(), ldap.Item.Id, ldap.Item.Version, authmethods.WithName("ldap-bar")) + require.NoError(err) + checkAuthMethod("update", ldap.Item, "ldap-bar", 2) + + ldap, err = amClient.Update(tc.Context(), ldap.Item.Id, ldap.Item.Version, authmethods.DefaultName()) + require.NoError(err) + checkAuthMethod("update", ldap.Item, "", 3) } func TestList(t *testing.T) { @@ -218,6 +255,29 @@ func TestList(t *testing.T) { ), ) + ldapAm, err := amClient.Create(tc.Context(), "ldap", global, + authmethods.WithName("ldap-foo"), + authmethods.WithLdapAuthMethodUrls([]string{"ldaps://ldap1"})) + require.NoError(err) + t.Cleanup(func() { + _, err = amClient.Delete(tc.Context(), ldapAm.Item.Id) + require.NoError(err) + }) + + result, err = amClient.List(tc.Context(), global) + require.NoError(err) + require.Len(result.Items, 5) + assert.Empty( + cmp.Diff( + result.Items, + []*authmethods.AuthMethod{genOIDCAM, genPWAM, pwAM.Item, oidcAM.Item, ldapAm.Item}, + cmpopts.IgnoreUnexported(authmethods.AuthMethod{}), + cmpopts.SortSlices(func(a, b *authmethods.AuthMethod) bool { + return a.Name < b.Name + }), + ), + ) + result, err = amClient.List(tc.Context(), global, authmethods.WithFilter(`"/item/attributes/client_id"=="client-id"`)) require.NoError(err) diff --git a/internal/tests/api/authmethods/classification_test.go b/internal/tests/api/authmethods/classification_test.go index 30d67f71f7..d80c97e208 100644 --- a/internal/tests/api/authmethods/classification_test.go +++ b/internal/tests/api/authmethods/classification_test.go @@ -171,6 +171,101 @@ func TestAuthMethod_Tags(t *testing.T) { }, }, }, + { + name: "validate-ldap-filtering", + testEvent: &eventlogger.Event{ + Type: "test", + CreatedAt: now, + Payload: &pb.AuthMethod{ + Id: "id", + ScopeId: "scope-id", + Name: &wrapperspb.StringValue{Value: "name"}, + Description: &wrapperspb.StringValue{Value: "description"}, + Type: "ldap", + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + StartTls: true, + InsecureTls: true, + DiscoverDn: true, + AnonGroupSearch: true, + UpnDomain: wrapperspb.String("upn-domain"), + Urls: []string{"ldaps://ldap1"}, + UserDn: wrapperspb.String("user-dn"), + UserAttr: wrapperspb.String("user-attr"), + UserFilter: wrapperspb.String("user-filter"), + EnableGroups: true, + GroupDn: wrapperspb.String("group-dn"), + GroupAttr: wrapperspb.String("group-attr"), + GroupFilter: wrapperspb.String("group-filter"), + Certificates: []string{"certs"}, + ClientCertificate: wrapperspb.String("client-cert"), + ClientCertificateKey: wrapperspb.String("client-cert-key"), + ClientCertificateKeyHmac: "client-cert-key-hmac", + BindDn: wrapperspb.String("bind-dn"), + BindPassword: wrapperspb.String("bind-password"), + BindPasswordHmac: "bind-password-hmac", + UseTokenGroups: true, + AccountAttributeMaps: []string{"map1"}, + }, + }, + AuthorizedActions: []string{"action-1", "action-2"}, + AuthorizedCollectionActions: map[string]*structpb.ListValue{ + "auth-methods": { + Values: []*structpb.Value{ + structpb.NewStringValue("create"), + structpb.NewStringValue("list"), + }, + }, + }, + }, + }, + wantEvent: &eventlogger.Event{ + Type: "test", + CreatedAt: now, + Payload: &pb.AuthMethod{ + Id: "id", + ScopeId: "scope-id", + Name: &wrapperspb.StringValue{Value: "name"}, + Description: &wrapperspb.StringValue{Value: "description"}, + Type: "ldap", + Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{ + LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{ + StartTls: true, + InsecureTls: true, + DiscoverDn: true, + AnonGroupSearch: true, + UpnDomain: wrapperspb.String("upn-domain"), + Urls: []string{"ldaps://ldap1"}, + UserDn: wrapperspb.String("user-dn"), + UserAttr: wrapperspb.String("user-attr"), + UserFilter: wrapperspb.String("user-filter"), + EnableGroups: true, + GroupDn: wrapperspb.String("group-dn"), + GroupAttr: wrapperspb.String("group-attr"), + GroupFilter: wrapperspb.String("group-filter"), + Certificates: []string{"certs"}, + ClientCertificate: wrapperspb.String("client-cert"), + ClientCertificateKey: wrapperspb.String(encrypt.RedactedData), + ClientCertificateKeyHmac: "client-cert-key-hmac", + BindDn: wrapperspb.String("bind-dn"), + BindPassword: wrapperspb.String(encrypt.RedactedData), + BindPasswordHmac: "bind-password-hmac", + UseTokenGroups: true, + AccountAttributeMaps: []string{"map1"}, + }, + }, + AuthorizedActions: []string{"action-1", "action-2"}, + AuthorizedCollectionActions: map[string]*structpb.ListValue{ + "auth-methods": { + Values: []*structpb.Value{ + structpb.NewStringValue("create"), + structpb.NewStringValue("list"), + }, + }, + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/sdk/pbs/controller/api/resources/accounts/account.pb.go b/sdk/pbs/controller/api/resources/accounts/account.pb.go index ce442a0757..800823a30a 100644 --- a/sdk/pbs/controller/api/resources/accounts/account.pb.go +++ b/sdk/pbs/controller/api/resources/accounts/account.pb.go @@ -59,6 +59,7 @@ type Account struct { // *Account_Attributes // *Account_PasswordAccountAttributes // *Account_OidcAccountAttributes + // *Account_LdapAccountAttributes Attrs isAccount_Attrs `protobuf_oneof:"attrs"` // Output only. managed_group_ids indicates IDs of the managed groups that currently contain this account ManagedGroupIds []string `protobuf:"bytes,110,rep,name=managed_group_ids,proto3" json:"managed_group_ids,omitempty" class:"public"` // @gotags: `class:"public"` @@ -189,6 +190,13 @@ func (x *Account) GetOidcAccountAttributes() *OidcAccountAttributes { return nil } +func (x *Account) GetLdapAccountAttributes() *LdapAccountAttributes { + if x, ok := x.GetAttrs().(*Account_LdapAccountAttributes); ok { + return x.LdapAccountAttributes + } + return nil +} + func (x *Account) GetManagedGroupIds() []string { if x != nil { return x.ManagedGroupIds @@ -220,12 +228,18 @@ type Account_OidcAccountAttributes struct { OidcAccountAttributes *OidcAccountAttributes `protobuf:"bytes,102,opt,name=oidc_account_attributes,json=oidcAccountAttributes,proto3,oneof"` } +type Account_LdapAccountAttributes struct { + LdapAccountAttributes *LdapAccountAttributes `protobuf:"bytes,103,opt,name=ldap_account_attributes,json=ldapAccountAttributes,proto3,oneof"` +} + func (*Account_Attributes) isAccount_Attrs() {} func (*Account_PasswordAccountAttributes) isAccount_Attrs() {} func (*Account_OidcAccountAttributes) isAccount_Attrs() {} +func (*Account_LdapAccountAttributes) isAccount_Attrs() {} + // Attributes associated only with Accounts with type "password". type PasswordAccountAttributes struct { state protoimpl.MessageState @@ -380,6 +394,102 @@ func (x *OidcAccountAttributes) GetUserinfoClaims() *structpb.Struct { return nil } +// Attributes associated only with Accounts with type "ldap". +type LdapAccountAttributes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // login_name of the authenticated user. This is the login_name (or username) + // entered by the user when authenticating (typically the uid or cn + // attribute). Account login names must be lower case. + LoginName string `protobuf:"bytes,100,opt,name=login_name,proto3" json:"login_name,omitempty" class:"sensitive"` // @gotags: `class:"sensitive"` + // Output only. full_name is a string that maps to the name attribute for the + // authenticated user. This attribute is updated every time a user + // successfully authenticates. + FullName string `protobuf:"bytes,110,opt,name=full_name,proto3" json:"full_name,omitempty" class:"sensitive"` // @gotags: `class:"sensitive"` + // Output only. email is a string that maps to the email address attribute for + // the authenticated user. This attribute is updated every time a user + // successfully authenticates. + Email string `protobuf:"bytes,120,opt,name=email,proto3" json:"email,omitempty" class:"sensitive"` // @gotags: `class:"sensitive"` + // Output only. dn is the distinguished name authenticated user's entry. Will + // be null until the user's first successful authentication. This attribute + // is updated every time a user successfully authenticates. + Dn string `protobuf:"bytes,130,opt,name=dn,proto3" json:"dn,omitempty" class:"public"` // @gotags: `class:"public"` + // Output only. member_of_groups are the json marshalled groups the + // authenticated user is a member of. Will be null until the user's first + // successful authentication. This attribute is updated every time a user + // successfully authenticates. + MemberOfGroups []string `protobuf:"bytes,140,rep,name=member_of_groups,json=memberOfGroups,proto3" json:"member_of_groups,omitempty" class:"public"` // @gotags: `class:"public"` +} + +func (x *LdapAccountAttributes) Reset() { + *x = LdapAccountAttributes{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_resources_accounts_v1_account_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LdapAccountAttributes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LdapAccountAttributes) ProtoMessage() {} + +func (x *LdapAccountAttributes) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_resources_accounts_v1_account_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LdapAccountAttributes.ProtoReflect.Descriptor instead. +func (*LdapAccountAttributes) Descriptor() ([]byte, []int) { + return file_controller_api_resources_accounts_v1_account_proto_rawDescGZIP(), []int{3} +} + +func (x *LdapAccountAttributes) GetLoginName() string { + if x != nil { + return x.LoginName + } + return "" +} + +func (x *LdapAccountAttributes) GetFullName() string { + if x != nil { + return x.FullName + } + return "" +} + +func (x *LdapAccountAttributes) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *LdapAccountAttributes) GetDn() string { + if x != nil { + return x.Dn + } + return "" +} + +func (x *LdapAccountAttributes) GetMemberOfGroups() []string { + if x != nil { + return x.MemberOfGroups + } + return nil +} + var File_controller_api_resources_accounts_v1_account_proto protoreflect.FileDescriptor var file_controller_api_resources_accounts_v1_account_proto_rawDesc = []byte{ @@ -402,7 +512,7 @@ var file_controller_api_resources_accounts_v1_account_proto_rawDesc = []byte{ 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x22, 0xd6, 0x07, 0x0a, 0x07, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x0e, + 0x74, 0x6f, 0x22, 0xec, 0x08, 0x0a, 0x07, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x43, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, @@ -457,46 +567,69 @@ var file_controller_api_resources_accounts_v1_account_proto_rawDesc = []byte{ 0x29, 0x01, 0x9a, 0xe3, 0x29, 0x04, 0x6f, 0x69, 0x64, 0x63, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x15, 0x6f, 0x69, 0x64, 0x63, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x65, 0x73, 0x12, 0x2c, 0x0a, 0x11, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x5f, 0x67, - 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x6e, 0x20, 0x03, 0x28, 0x09, 0x52, 0x11, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, - 0x73, 0x12, 0x2f, 0x0a, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, - 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xac, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, - 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x42, 0x07, 0x0a, 0x05, 0x61, 0x74, 0x74, 0x72, 0x73, 0x22, 0xa7, 0x01, 0x0a, 0x19, - 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x41, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x4a, 0x0a, 0x0a, 0x6c, 0x6f, 0x67, - 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x42, 0x2a, 0xa0, - 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x22, 0x0a, 0x15, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x65, 0x73, 0x2e, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x09, - 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x0a, 0x6c, 0x6f, 0x67, 0x69, 0x6e, - 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x3e, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, - 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x04, 0xa0, 0xda, 0x29, 0x01, 0x52, 0x08, 0x70, 0x61, 0x73, - 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x88, 0x02, 0x0a, 0x15, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x63, + 0x74, 0x65, 0x73, 0x12, 0x93, 0x01, 0x0a, 0x17, 0x6c, 0x64, 0x61, 0x70, 0x5f, 0x61, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, + 0x67, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3b, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, + 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, + 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x64, 0x61, + 0x70, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x73, 0x42, 0x1c, 0xa0, 0xda, 0x29, 0x01, 0x9a, 0xe3, 0x29, 0x04, 0x6c, 0x64, 0x61, 0x70, + 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, + 0x48, 0x00, 0x52, 0x15, 0x6c, 0x64, 0x61, 0x70, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x41, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x2c, 0x0a, 0x11, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x64, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x6e, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x11, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x5f, 0x67, 0x72, + 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x73, 0x12, 0x2f, 0x0a, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, + 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xac, 0x02, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, + 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x07, 0x0a, 0x05, 0x61, 0x74, 0x74, 0x72, + 0x73, 0x22, 0xa7, 0x01, 0x0a, 0x19, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, - 0x1c, 0x0a, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x18, 0x50, 0x20, 0x01, 0x28, 0x09, 0x42, - 0x04, 0xa0, 0xda, 0x29, 0x01, 0x52, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x12, 0x1e, 0x0a, - 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x5a, 0x20, 0x01, 0x28, 0x09, 0x42, 0x04, - 0xa0, 0xda, 0x29, 0x01, 0x52, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x1c, 0x0a, - 0x09, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, - 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x6e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, - 0x6c, 0x12, 0x3a, 0x0a, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x63, 0x6c, 0x61, 0x69, 0x6d, - 0x73, 0x18, 0x78, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, - 0x52, 0x0b, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x12, 0x41, 0x0a, - 0x0f, 0x75, 0x73, 0x65, 0x72, 0x69, 0x6e, 0x66, 0x6f, 0x5f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x73, - 0x18, 0x82, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, - 0x52, 0x0e, 0x75, 0x73, 0x65, 0x72, 0x69, 0x6e, 0x66, 0x6f, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x73, - 0x42, 0x52, 0x5a, 0x50, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, - 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, - 0x79, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, - 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x73, 0x2f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x3b, 0x61, 0x63, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x4a, 0x0a, 0x0a, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x2a, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x22, 0x0a, 0x15, 0x61, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x09, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x52, + 0x0a, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x3e, 0x0a, 0x08, 0x70, + 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x04, 0xa0, 0xda, 0x29, + 0x01, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x88, 0x02, 0x0a, 0x15, + 0x4f, 0x69, 0x64, 0x63, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1c, 0x0a, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x18, + 0x50, 0x20, 0x01, 0x28, 0x09, 0x42, 0x04, 0xa0, 0xda, 0x29, 0x01, 0x52, 0x06, 0x69, 0x73, 0x73, + 0x75, 0x65, 0x72, 0x12, 0x1e, 0x0a, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x5a, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x04, 0xa0, 0xda, 0x29, 0x01, 0x52, 0x07, 0x73, 0x75, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x6e, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x3a, 0x0a, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x5f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x18, 0x78, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x0b, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x43, 0x6c, 0x61, + 0x69, 0x6d, 0x73, 0x12, 0x41, 0x0a, 0x0f, 0x75, 0x73, 0x65, 0x72, 0x69, 0x6e, 0x66, 0x6f, 0x5f, + 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x18, 0x82, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x0e, 0x75, 0x73, 0x65, 0x72, 0x69, 0x6e, 0x66, 0x6f, + 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x22, 0xd3, 0x01, 0x0a, 0x15, 0x4c, 0x64, 0x61, 0x70, 0x41, + 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, + 0x12, 0x4a, 0x0a, 0x0a, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x64, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x2a, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x22, 0x0a, 0x15, + 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x6c, 0x6f, 0x67, 0x69, 0x6e, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x09, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x4e, 0x61, 0x6d, 0x65, + 0x52, 0x0a, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, + 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x6e, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, + 0x61, 0x69, 0x6c, 0x18, 0x78, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, + 0x12, 0x0f, 0x0a, 0x02, 0x64, 0x6e, 0x18, 0x82, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x64, + 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x5f, 0x6f, 0x66, 0x5f, 0x67, + 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x8c, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x6d, 0x65, + 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x42, 0x52, 0x5a, 0x50, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, + 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x73, 0x64, + 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x61, + 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x3b, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -511,33 +644,35 @@ func file_controller_api_resources_accounts_v1_account_proto_rawDescGZIP() []byt return file_controller_api_resources_accounts_v1_account_proto_rawDescData } -var file_controller_api_resources_accounts_v1_account_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_controller_api_resources_accounts_v1_account_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_controller_api_resources_accounts_v1_account_proto_goTypes = []interface{}{ (*Account)(nil), // 0: controller.api.resources.accounts.v1.Account (*PasswordAccountAttributes)(nil), // 1: controller.api.resources.accounts.v1.PasswordAccountAttributes (*OidcAccountAttributes)(nil), // 2: controller.api.resources.accounts.v1.OidcAccountAttributes - (*scopes.ScopeInfo)(nil), // 3: controller.api.resources.scopes.v1.ScopeInfo - (*wrapperspb.StringValue)(nil), // 4: google.protobuf.StringValue - (*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 6: google.protobuf.Struct + (*LdapAccountAttributes)(nil), // 3: controller.api.resources.accounts.v1.LdapAccountAttributes + (*scopes.ScopeInfo)(nil), // 4: controller.api.resources.scopes.v1.ScopeInfo + (*wrapperspb.StringValue)(nil), // 5: google.protobuf.StringValue + (*timestamppb.Timestamp)(nil), // 6: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 7: google.protobuf.Struct } var file_controller_api_resources_accounts_v1_account_proto_depIdxs = []int32{ - 3, // 0: controller.api.resources.accounts.v1.Account.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo - 4, // 1: controller.api.resources.accounts.v1.Account.name:type_name -> google.protobuf.StringValue - 4, // 2: controller.api.resources.accounts.v1.Account.description:type_name -> google.protobuf.StringValue - 5, // 3: controller.api.resources.accounts.v1.Account.created_time:type_name -> google.protobuf.Timestamp - 5, // 4: controller.api.resources.accounts.v1.Account.updated_time:type_name -> google.protobuf.Timestamp - 6, // 5: controller.api.resources.accounts.v1.Account.attributes:type_name -> google.protobuf.Struct + 4, // 0: controller.api.resources.accounts.v1.Account.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo + 5, // 1: controller.api.resources.accounts.v1.Account.name:type_name -> google.protobuf.StringValue + 5, // 2: controller.api.resources.accounts.v1.Account.description:type_name -> google.protobuf.StringValue + 6, // 3: controller.api.resources.accounts.v1.Account.created_time:type_name -> google.protobuf.Timestamp + 6, // 4: controller.api.resources.accounts.v1.Account.updated_time:type_name -> google.protobuf.Timestamp + 7, // 5: controller.api.resources.accounts.v1.Account.attributes:type_name -> google.protobuf.Struct 1, // 6: controller.api.resources.accounts.v1.Account.password_account_attributes:type_name -> controller.api.resources.accounts.v1.PasswordAccountAttributes 2, // 7: controller.api.resources.accounts.v1.Account.oidc_account_attributes:type_name -> controller.api.resources.accounts.v1.OidcAccountAttributes - 4, // 8: controller.api.resources.accounts.v1.PasswordAccountAttributes.password:type_name -> google.protobuf.StringValue - 6, // 9: controller.api.resources.accounts.v1.OidcAccountAttributes.token_claims:type_name -> google.protobuf.Struct - 6, // 10: controller.api.resources.accounts.v1.OidcAccountAttributes.userinfo_claims:type_name -> google.protobuf.Struct - 11, // [11:11] is the sub-list for method output_type - 11, // [11:11] is the sub-list for method input_type - 11, // [11:11] is the sub-list for extension type_name - 11, // [11:11] is the sub-list for extension extendee - 0, // [0:11] is the sub-list for field type_name + 3, // 8: controller.api.resources.accounts.v1.Account.ldap_account_attributes:type_name -> controller.api.resources.accounts.v1.LdapAccountAttributes + 5, // 9: controller.api.resources.accounts.v1.PasswordAccountAttributes.password:type_name -> google.protobuf.StringValue + 7, // 10: controller.api.resources.accounts.v1.OidcAccountAttributes.token_claims:type_name -> google.protobuf.Struct + 7, // 11: controller.api.resources.accounts.v1.OidcAccountAttributes.userinfo_claims:type_name -> google.protobuf.Struct + 12, // [12:12] is the sub-list for method output_type + 12, // [12:12] is the sub-list for method input_type + 12, // [12:12] is the sub-list for extension type_name + 12, // [12:12] is the sub-list for extension extendee + 0, // [0:12] is the sub-list for field type_name } func init() { file_controller_api_resources_accounts_v1_account_proto_init() } @@ -582,11 +717,24 @@ func file_controller_api_resources_accounts_v1_account_proto_init() { return nil } } + file_controller_api_resources_accounts_v1_account_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LdapAccountAttributes); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_controller_api_resources_accounts_v1_account_proto_msgTypes[0].OneofWrappers = []interface{}{ (*Account_Attributes)(nil), (*Account_PasswordAccountAttributes)(nil), (*Account_OidcAccountAttributes)(nil), + (*Account_LdapAccountAttributes)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -594,7 +742,7 @@ func file_controller_api_resources_accounts_v1_account_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_api_resources_accounts_v1_account_proto_rawDesc, NumEnums: 0, - NumMessages: 3, + NumMessages: 4, NumExtensions: 0, NumServices: 0, }, diff --git a/sdk/pbs/controller/api/resources/authmethods/auth_method.pb.go b/sdk/pbs/controller/api/resources/authmethods/auth_method.pb.go index 7a1c9d8feb..65d66bc588 100644 --- a/sdk/pbs/controller/api/resources/authmethods/auth_method.pb.go +++ b/sdk/pbs/controller/api/resources/authmethods/auth_method.pb.go @@ -59,6 +59,7 @@ type AuthMethod struct { // *AuthMethod_Attributes // *AuthMethod_PasswordAuthMethodAttributes // *AuthMethod_OidcAuthMethodsAttributes + // *AuthMethod_LdapAuthMethodsAttributes Attrs isAuthMethod_Attrs `protobuf_oneof:"attrs"` // Output only. Whether this auth method is the primary auth method for it's scope. // To change this value update the primary_auth_method_id field on the scope. @@ -192,6 +193,13 @@ func (x *AuthMethod) GetOidcAuthMethodsAttributes() *OidcAuthMethodAttributes { return nil } +func (x *AuthMethod) GetLdapAuthMethodsAttributes() *LdapAuthMethodAttributes { + if x, ok := x.GetAttrs().(*AuthMethod_LdapAuthMethodsAttributes); ok { + return x.LdapAuthMethodsAttributes + } + return nil +} + func (x *AuthMethod) GetIsPrimary() bool { if x != nil { return x.IsPrimary @@ -230,12 +238,18 @@ type AuthMethod_OidcAuthMethodsAttributes struct { OidcAuthMethodsAttributes *OidcAuthMethodAttributes `protobuf:"bytes,102,opt,name=oidc_auth_methods_attributes,json=oidcAuthMethodsAttributes,proto3,oneof"` } +type AuthMethod_LdapAuthMethodsAttributes struct { + LdapAuthMethodsAttributes *LdapAuthMethodAttributes `protobuf:"bytes,103,opt,name=ldap_auth_methods_attributes,json=ldapAuthMethodsAttributes,proto3,oneof"` +} + func (*AuthMethod_Attributes) isAuthMethod_Attrs() {} func (*AuthMethod_PasswordAuthMethodAttributes) isAuthMethod_Attrs() {} func (*AuthMethod_OidcAuthMethodsAttributes) isAuthMethod_Attrs() {} +func (*AuthMethod_LdapAuthMethodsAttributes) isAuthMethod_Attrs() {} + // The attributes of a password typed auth method. type PasswordAuthMethodAttributes struct { state protoimpl.MessageState @@ -787,6 +801,297 @@ func (x *OidcAuthMethodAuthenticateTokenResponse) GetStatus() string { return "" } +// The attributes of an LDAP typed auth method. +type LdapAuthMethodAttributes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Output only. The state of the auth method. Will be "inactive", + // "active-private", or "active-public". + State string `protobuf:"bytes,10,opt,name=state,proto3" json:"state,omitempty" class:"public"` // @gotags: `class:"public"` + // start_tls if true, issues a StartTLS command after establishing an + // unencrypted connection. Defaults to false. + StartTls bool `protobuf:"varint,20,opt,name=start_tls,proto3" json:"start_tls,omitempty" class:"public"` // @gotags: `class:"public"` + // insecure_tls if true, skips LDAP server SSL certificate validation - + // insecure and use with caution. Defaults to false. + InsecureTls bool `protobuf:"varint,30,opt,name=insecure_tls,proto3" json:"insecure_tls,omitempty" class:"public"` // @gotags: `class:"public"` + // discover_dn if true, use anon bind to discover the bind DN of a user. + // Defaults to false. + DiscoverDn bool `protobuf:"varint,40,opt,name=discover_dn,proto3" json:"discover_dn,omitempty" class:"public"` // @gotags: `class:"public"` + // anon_group_search if true, use anon bind when performing LDAP group + // searches. Defaults to false. + AnonGroupSearch bool `protobuf:"varint,50,opt,name=anon_group_search,proto3" json:"anon_group_search,omitempty" class:"public"` // @gotags: `class:"public"` + // upn_domain is the userPrincipalDomain used to construct the UPN string for + // the authenticating user. The constructed UPN will appear as + // [username]@UPNDomain Example: example.com, which will cause Boundary to + // bind as username@example.com when authenticating the user. + UpnDomain *wrapperspb.StringValue `protobuf:"bytes,60,opt,name=upn_domain,proto3" json:"upn_domain,omitempty" class:"public"` // @gotags: `class:"public"` + // urls are the LDAP URLS that specify LDAP servers to connection to. There + // must be at lease on URL for each LDAP auth method. When attempting to + // connect, the URLs are tried in the order specified. These are Value Objects + // that will be stored as Url messages, and are operated on as a complete set + // (not individually). + Urls []string `protobuf:"bytes,70,rep,name=urls,proto3" json:"urls,omitempty" class:"public"` // @gotags: `class:"public"` + // user_dn (optional) is the base DN under which to perform user search. + // Example: ou=Users,dc=example,dc=com + UserDn *wrapperspb.StringValue `protobuf:"bytes,80,opt,name=user_dn,proto3" json:"user_dn,omitempty" class:"public"` // @gotags: `class:"public"` + // user_attr (optional) is the attribute on user attribute entry matching the + // username passed when authenticating. Examples: cn, uid + UserAttr *wrapperspb.StringValue `protobuf:"bytes,90,opt,name=user_attr,proto3" json:"user_attr,omitempty" class:"public"` // @gotags: `class:"public"` + // user_filter (optional) is a go template used to construct a LDAP user + // search filter. The template can access the following context variables: + // [UserAttr, Username]. The default userfilter is + // ({{.UserAttr}}={{.Username}}) or + // (userPrincipalName={{.Username}}@UPNDomain) if the upndomain parameter is + // set. + UserFilter *wrapperspb.StringValue `protobuf:"bytes,100,opt,name=user_filter,proto3" json:"user_filter,omitempty" class:"public"` // @gotags: `class:"public"` + // enable_groups if true, an authenticated user's groups will be found during + // authentication. Defaults to false. + EnableGroups bool `protobuf:"varint,110,opt,name=enable_groups,proto3" json:"enable_groups,omitempty" class:"public"` // @gotags: `class:"public"` + // group_dn (optional) is the base DN under which to perform user search. + // Example: ou=Groups,dc=example,dc=com + // + // Note: there is no default, so no base dn will be used for group searches if + // it's not specified. + GroupDn *wrapperspb.StringValue `protobuf:"bytes,120,opt,name=group_dn,proto3" json:"group_dn,omitempty" class:"public"` // @gotags: `class:"public"` + // group_attr (optional) is the LDAP attribute to follow on objects returned + // by GroupFilter in order to enumerate user group membership. Examples: for + // GroupFilter queries returning group objects, use: cn. For queries returning + // user objects, use: memberOf. The default is cn. + GroupAttr *wrapperspb.StringValue `protobuf:"bytes,130,opt,name=group_attr,proto3" json:"group_attr,omitempty" class:"public"` // @gotags: `class:"public"` + // group_filter (optional) is a Go template used when constructing the group + // membership query. The template can access the following context variables: + // [UserDN, Username]. The default is + // (|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}})), + // which is compatible with several common directory schemas. + GroupFilter *wrapperspb.StringValue `protobuf:"bytes,140,opt,name=group_filter,proto3" json:"group_filter,omitempty" class:"public"` // @gotags: `class:"public"` + // certificates are optional PEM encoded x509 certificates in ASN.1 DER form + // that can be used as trust anchors when connecting to an LDAP provider. + // These are Value Objects that will be stored as Certificate messages, and + // are operatated on as a complete set (not individually). + Certificates []string `protobuf:"bytes,150,rep,name=certificates,proto3" json:"certificates,omitempty" class:"public"` // @gotags: `class:"public"` + // client_certificate is the optional certificate encoded as PEM. It must be + // set if an optional client_certificate_key specified + ClientCertificate *wrapperspb.StringValue `protobuf:"bytes,160,opt,name=client_certificate,proto3" json:"client_certificate,omitempty" class:"public"` // @gotags: `class:"public"` + // Input only. The client_certificate_key (optional) is the plain-text of the + // certificate key data encoded as PEM. + ClientCertificateKey *wrapperspb.StringValue `protobuf:"bytes,170,opt,name=client_certificate_key,proto3" json:"client_certificate_key,omitempty" class:"secret"` // @gotags: `class:"secret"` + // Output only. The HMAC'd value of the client certificate key to indicate + // whether the certificate key has changed. + ClientCertificateKeyHmac string `protobuf:"bytes,180,opt,name=client_certificate_key_hmac,proto3" json:"client_certificate_key_hmac,omitempty" class:"public"` // @gotags: `class:"public"` + // bind_dn (optional) is the distinguished name of entry to bind when + // performing user and group search. Example: + // cn=vault,ou=Users,dc=example,dc=com + BindDn *wrapperspb.StringValue `protobuf:"bytes,190,opt,name=bind_dn,proto3" json:"bind_dn,omitempty" class:"public"` // @gotags: `class:"public"` + // Input only. The bind_password (optional) is the password to use along with + // binddn when performing user search. + BindPassword *wrapperspb.StringValue `protobuf:"bytes,200,opt,name=bind_password,proto3" json:"bind_password,omitempty" class:"secret"` // @gotags: `class:"secret"` + // Output only. The HMAC'd value of the bind password to indicate + // whether the password has changed. + BindPasswordHmac string `protobuf:"bytes,210,opt,name=bind_password_hmac,proto3" json:"bind_password_hmac,omitempty" class:"public"` // @gotags: `class:"public"` + UseTokenGroups bool `protobuf:"varint,220,opt,name=use_token_groups,proto3" json:"use_token_groups,omitempty" class:"public"` // @gotags: `class:"public"` + // account_attribute_maps are optional attribute maps from custom attributes + // to the standard attributes of fullname and email. These maps are + // represented as key=value where the key equals the from_attribute and the + // value equals the to_attribute. For example "preferredName=fullName". All + // attribute names are case insensitive. + AccountAttributeMaps []string `protobuf:"bytes,230,rep,name=account_attribute_maps,proto3" json:"account_attribute_maps,omitempty" class:"public"` // @gotags: `class:"public"` +} + +func (x *LdapAuthMethodAttributes) Reset() { + *x = LdapAuthMethodAttributes{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_resources_authmethods_v1_auth_method_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LdapAuthMethodAttributes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LdapAuthMethodAttributes) ProtoMessage() {} + +func (x *LdapAuthMethodAttributes) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_resources_authmethods_v1_auth_method_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LdapAuthMethodAttributes.ProtoReflect.Descriptor instead. +func (*LdapAuthMethodAttributes) Descriptor() ([]byte, []int) { + return file_controller_api_resources_authmethods_v1_auth_method_proto_rawDescGZIP(), []int{8} +} + +func (x *LdapAuthMethodAttributes) GetState() string { + if x != nil { + return x.State + } + return "" +} + +func (x *LdapAuthMethodAttributes) GetStartTls() bool { + if x != nil { + return x.StartTls + } + return false +} + +func (x *LdapAuthMethodAttributes) GetInsecureTls() bool { + if x != nil { + return x.InsecureTls + } + return false +} + +func (x *LdapAuthMethodAttributes) GetDiscoverDn() bool { + if x != nil { + return x.DiscoverDn + } + return false +} + +func (x *LdapAuthMethodAttributes) GetAnonGroupSearch() bool { + if x != nil { + return x.AnonGroupSearch + } + return false +} + +func (x *LdapAuthMethodAttributes) GetUpnDomain() *wrapperspb.StringValue { + if x != nil { + return x.UpnDomain + } + return nil +} + +func (x *LdapAuthMethodAttributes) GetUrls() []string { + if x != nil { + return x.Urls + } + return nil +} + +func (x *LdapAuthMethodAttributes) GetUserDn() *wrapperspb.StringValue { + if x != nil { + return x.UserDn + } + return nil +} + +func (x *LdapAuthMethodAttributes) GetUserAttr() *wrapperspb.StringValue { + if x != nil { + return x.UserAttr + } + return nil +} + +func (x *LdapAuthMethodAttributes) GetUserFilter() *wrapperspb.StringValue { + if x != nil { + return x.UserFilter + } + return nil +} + +func (x *LdapAuthMethodAttributes) GetEnableGroups() bool { + if x != nil { + return x.EnableGroups + } + return false +} + +func (x *LdapAuthMethodAttributes) GetGroupDn() *wrapperspb.StringValue { + if x != nil { + return x.GroupDn + } + return nil +} + +func (x *LdapAuthMethodAttributes) GetGroupAttr() *wrapperspb.StringValue { + if x != nil { + return x.GroupAttr + } + return nil +} + +func (x *LdapAuthMethodAttributes) GetGroupFilter() *wrapperspb.StringValue { + if x != nil { + return x.GroupFilter + } + return nil +} + +func (x *LdapAuthMethodAttributes) GetCertificates() []string { + if x != nil { + return x.Certificates + } + return nil +} + +func (x *LdapAuthMethodAttributes) GetClientCertificate() *wrapperspb.StringValue { + if x != nil { + return x.ClientCertificate + } + return nil +} + +func (x *LdapAuthMethodAttributes) GetClientCertificateKey() *wrapperspb.StringValue { + if x != nil { + return x.ClientCertificateKey + } + return nil +} + +func (x *LdapAuthMethodAttributes) GetClientCertificateKeyHmac() string { + if x != nil { + return x.ClientCertificateKeyHmac + } + return "" +} + +func (x *LdapAuthMethodAttributes) GetBindDn() *wrapperspb.StringValue { + if x != nil { + return x.BindDn + } + return nil +} + +func (x *LdapAuthMethodAttributes) GetBindPassword() *wrapperspb.StringValue { + if x != nil { + return x.BindPassword + } + return nil +} + +func (x *LdapAuthMethodAttributes) GetBindPasswordHmac() string { + if x != nil { + return x.BindPasswordHmac + } + return "" +} + +func (x *LdapAuthMethodAttributes) GetUseTokenGroups() bool { + if x != nil { + return x.UseTokenGroups + } + return false +} + +func (x *LdapAuthMethodAttributes) GetAccountAttributeMaps() []string { + if x != nil { + return x.AccountAttributeMaps + } + return nil +} + var File_controller_api_resources_authmethods_v1_auth_method_proto protoreflect.FileDescriptor var file_controller_api_resources_authmethods_v1_auth_method_proto_rawDesc = []byte{ @@ -809,7 +1114,7 @@ var file_controller_api_resources_authmethods_v1_auth_method_proto_rawDesc = []b 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x77, 0x72, - 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xdf, 0x09, 0x0a, + 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x84, 0x0b, 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, @@ -865,163 +1170,314 @@ var file_controller_api_resources_authmethods_v1_auth_method_proto_rawDesc = []b 0x1c, 0xa0, 0xda, 0x29, 0x01, 0x9a, 0xe3, 0x29, 0x04, 0x6f, 0x69, 0x64, 0x63, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x19, 0x6f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, - 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x73, - 0x5f, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x6e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, - 0x69, 0x73, 0x5f, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x2f, 0x0a, 0x12, 0x61, 0x75, + 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0xa2, 0x01, 0x0a, 0x1c, 0x6c, + 0x64, 0x61, 0x70, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, + 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x67, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x41, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, + 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x64, 0x61, 0x70, + 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x73, 0x42, 0x1c, 0xa0, 0xda, 0x29, 0x01, 0x9a, 0xe3, 0x29, 0x04, 0x6c, 0x64, + 0x61, 0x70, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, + 0x41, 0x4c, 0x48, 0x00, 0x52, 0x19, 0x6c, 0x64, 0x61, 0x70, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x73, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, + 0x1e, 0x0a, 0x0a, 0x69, 0x73, 0x5f, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x6e, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69, 0x73, 0x5f, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x12, + 0x2f, 0x0a, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xac, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x18, 0xac, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, - 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x9b, 0x01, 0x0a, 0x1d, - 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xb6, 0x02, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x54, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, - 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, - 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, - 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, - 0x69, 0x7a, 0x65, 0x64, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x1d, 0x61, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x6a, 0x0a, 0x20, 0x41, 0x75, 0x74, - 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, - 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, - 0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x12, 0x9b, 0x01, 0x0a, 0x1d, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, + 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x18, 0xb6, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x54, 0x2e, 0x63, 0x6f, 0x6e, 0x74, + 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, + 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x2e, 0x41, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, + 0x1d, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x6c, 0x6c, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x6a, + 0x0a, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x43, 0x6f, 0x6c, 0x6c, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x07, 0x0a, 0x05, 0x61, 0x74, + 0x74, 0x72, 0x73, 0x22, 0x83, 0x02, 0x0a, 0x1c, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, + 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x73, 0x12, 0x74, 0x0a, 0x15, 0x6d, 0x69, 0x6e, 0x5f, 0x6c, 0x6f, 0x67, 0x69, + 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x0d, 0x42, 0x3e, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x36, 0x0a, 0x20, 0x61, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x6d, 0x69, 0x6e, 0x5f, 0x6c, 0x6f, + 0x67, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x12, + 0x12, 0x4d, 0x69, 0x6e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x4c, 0x65, 0x6e, + 0x67, 0x74, 0x68, 0x52, 0x15, 0x6d, 0x69, 0x6e, 0x5f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x6e, + 0x61, 0x6d, 0x65, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x12, 0x6d, 0x0a, 0x13, 0x6d, 0x69, + 0x6e, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, + 0x68, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x3b, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, + 0x33, 0x0a, 0x1e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x6d, 0x69, + 0x6e, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, + 0x68, 0x12, 0x11, 0x4d, 0x69, 0x6e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x4c, 0x65, + 0x6e, 0x67, 0x74, 0x68, 0x52, 0x13, 0x6d, 0x69, 0x6e, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, + 0x72, 0x64, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x22, 0xe6, 0x09, 0x0a, 0x18, 0x4f, 0x69, + 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x59, 0x0a, 0x06, + 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, + 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x23, 0xa0, 0xda, 0x29, 0x01, + 0xc2, 0xdd, 0x29, 0x1b, 0x0a, 0x11, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, + 0x2e, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x12, 0x06, 0x49, 0x73, 0x73, 0x75, 0x65, 0x72, 0x52, + 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x12, 0x64, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x5f, 0x69, 0x64, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, + 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x28, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, + 0x29, 0x20, 0x0a, 0x14, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x63, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x12, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x49, 0x64, 0x52, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x12, 0x74, 0x0a, + 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x28, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x42, 0x30, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x28, 0x0a, 0x18, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, + 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, + 0x63, 0x72, 0x65, 0x74, 0x52, 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x63, + 0x72, 0x65, 0x74, 0x12, 0x2e, 0x0a, 0x12, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, + 0x63, 0x72, 0x65, 0x74, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x18, 0x32, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x12, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x5f, 0x68, + 0x6d, 0x61, 0x63, 0x12, 0x5c, 0x0a, 0x07, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x67, 0x65, 0x18, 0x3c, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x55, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x42, 0x24, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1c, 0x0a, 0x12, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x67, 0x65, + 0x12, 0x06, 0x4d, 0x61, 0x78, 0x41, 0x67, 0x65, 0x52, 0x07, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x67, + 0x65, 0x12, 0x64, 0x0a, 0x12, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x61, 0x6c, 0x67, + 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x73, 0x18, 0x46, 0x20, 0x03, 0x28, 0x09, 0x42, 0x34, 0xa0, + 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x2c, 0x0a, 0x1d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x73, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x61, 0x6c, 0x67, 0x6f, + 0x72, 0x69, 0x74, 0x68, 0x6d, 0x73, 0x12, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x41, + 0x6c, 0x67, 0x73, 0x52, 0x12, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x61, 0x6c, 0x67, + 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x73, 0x12, 0x71, 0x0a, 0x0e, 0x61, 0x70, 0x69, 0x5f, 0x75, + 0x72, 0x6c, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x50, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x2b, 0xa0, + 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x23, 0x0a, 0x19, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x5f, 0x75, 0x72, 0x6c, 0x5f, 0x70, 0x72, 0x65, 0x66, + 0x69, 0x78, 0x12, 0x06, 0x41, 0x70, 0x69, 0x55, 0x72, 0x6c, 0x52, 0x0e, 0x61, 0x70, 0x69, 0x5f, + 0x75, 0x72, 0x6c, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x22, 0x0a, 0x0c, 0x63, 0x61, + 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x5a, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x63, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x12, 0x53, + 0x0a, 0x0c, 0x69, 0x64, 0x70, 0x5f, 0x63, 0x61, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x73, 0x18, 0x64, + 0x20, 0x03, 0x28, 0x09, 0x42, 0x2f, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x27, 0x0a, 0x17, + 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x69, 0x64, 0x70, 0x5f, 0x63, + 0x61, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x73, 0x12, 0x0c, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x73, 0x52, 0x0c, 0x69, 0x64, 0x70, 0x5f, 0x63, 0x61, 0x5f, 0x63, 0x65, + 0x72, 0x74, 0x73, 0x12, 0x5f, 0x0a, 0x11, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x61, + 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x6e, 0x20, 0x03, 0x28, 0x09, 0x42, 0x31, + 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x29, 0x0a, 0x1c, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x73, 0x2e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x61, 0x75, 0x64, + 0x69, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x09, 0x41, 0x75, 0x64, 0x43, 0x6c, 0x61, 0x69, 0x6d, + 0x73, 0x52, 0x11, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x61, 0x75, 0x64, 0x69, 0x65, + 0x6e, 0x63, 0x65, 0x73, 0x12, 0x56, 0x0a, 0x0d, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x5f, 0x73, + 0x63, 0x6f, 0x70, 0x65, 0x73, 0x18, 0x70, 0x20, 0x03, 0x28, 0x09, 0x42, 0x30, 0xa0, 0xda, 0x29, + 0x01, 0xc2, 0xdd, 0x29, 0x28, 0x0a, 0x18, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x73, 0x2e, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x12, + 0x0c, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x52, 0x0d, 0x63, + 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x12, 0x69, 0x0a, 0x12, + 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x5f, 0x6d, 0x61, + 0x70, 0x73, 0x18, 0x71, 0x20, 0x03, 0x28, 0x09, 0x42, 0x39, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, + 0x29, 0x31, 0x0a, 0x1d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x61, + 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x5f, 0x6d, 0x61, 0x70, + 0x73, 0x12, 0x10, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x4d, + 0x61, 0x70, 0x73, 0x52, 0x12, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x63, 0x6c, 0x61, + 0x69, 0x6d, 0x5f, 0x6d, 0x61, 0x70, 0x73, 0x12, 0x58, 0x0a, 0x24, 0x64, 0x69, 0x73, 0x61, 0x62, + 0x6c, 0x65, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x65, 0x64, 0x5f, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x78, 0x20, 0x01, 0x28, 0x08, 0x42, 0x04, 0xa0, 0xda, 0x29, 0x01, 0x52, 0x24, 0x64, 0x69, 0x73, + 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x65, 0x64, 0x5f, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x1f, 0x0a, 0x07, 0x64, 0x72, 0x79, 0x5f, 0x72, 0x75, 0x6e, 0x18, 0x82, 0x01, 0x20, + 0x01, 0x28, 0x08, 0x42, 0x04, 0xa0, 0xda, 0x29, 0x01, 0x52, 0x07, 0x64, 0x72, 0x79, 0x5f, 0x72, + 0x75, 0x6e, 0x22, 0x61, 0x0a, 0x27, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, + 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, + 0x08, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x75, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x5f, 0x69, 0x64, 0x22, 0xb7, 0x01, 0x0a, 0x29, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, + 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, + 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x12, 0x2c, 0x0a, 0x11, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x64, 0x65, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x32, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x75, 0x72, 0x69, 0x22, + 0x5c, 0x0a, 0x2a, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x61, 0x6c, + 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, + 0x12, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x5f, + 0x75, 0x72, 0x6c, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x66, 0x69, 0x6e, 0x61, 0x6c, + 0x5f, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x72, 0x6c, 0x22, 0x44, 0x0a, + 0x26, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, + 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x5f, 0x69, 0x64, 0x22, 0x41, 0x0a, 0x27, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, + 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0xc1, 0x11, 0x0a, 0x18, 0x4c, 0x64, 0x61, 0x70, 0x41, + 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x73, 0x12, 0x42, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x2c, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x24, 0x0a, 0x10, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, + 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x46, 0x0a, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, + 0x5f, 0x74, 0x6c, 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, 0x08, 0x42, 0x28, 0xa0, 0xda, 0x29, 0x01, + 0xc2, 0xdd, 0x29, 0x20, 0x0a, 0x14, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, + 0x2e, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x6c, 0x73, 0x12, 0x08, 0x53, 0x74, 0x61, 0x72, + 0x74, 0x54, 0x6c, 0x73, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x6c, 0x73, 0x12, + 0x52, 0x0a, 0x0c, 0x69, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x5f, 0x74, 0x6c, 0x73, 0x18, + 0x1e, 0x20, 0x01, 0x28, 0x08, 0x42, 0x2e, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x26, 0x0a, + 0x17, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x69, 0x6e, 0x73, 0x65, + 0x63, 0x75, 0x72, 0x65, 0x5f, 0x74, 0x6c, 0x73, 0x12, 0x0b, 0x49, 0x6e, 0x73, 0x65, 0x63, 0x75, + 0x72, 0x65, 0x54, 0x6c, 0x73, 0x52, 0x0c, 0x69, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x5f, + 0x74, 0x6c, 0x73, 0x12, 0x4e, 0x0a, 0x0b, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x5f, + 0x64, 0x6e, 0x18, 0x28, 0x20, 0x01, 0x28, 0x08, 0x42, 0x2c, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, + 0x29, 0x24, 0x0a, 0x16, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x64, + 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x5f, 0x64, 0x6e, 0x12, 0x0a, 0x44, 0x69, 0x73, 0x63, + 0x6f, 0x76, 0x65, 0x72, 0x44, 0x6e, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, + 0x5f, 0x64, 0x6e, 0x12, 0x65, 0x0a, 0x11, 0x61, 0x6e, 0x6f, 0x6e, 0x5f, 0x67, 0x72, 0x6f, 0x75, + 0x70, 0x5f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x18, 0x32, 0x20, 0x01, 0x28, 0x08, 0x42, 0x37, + 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x2f, 0x0a, 0x1c, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x73, 0x2e, 0x61, 0x6e, 0x6f, 0x6e, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, + 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x0f, 0x41, 0x6e, 0x6f, 0x6e, 0x47, 0x72, 0x6f, 0x75, + 0x70, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x11, 0x61, 0x6e, 0x6f, 0x6e, 0x5f, 0x67, 0x72, + 0x6f, 0x75, 0x70, 0x5f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x68, 0x0a, 0x0a, 0x75, 0x70, + 0x6e, 0x5f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x3c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x07, 0x0a, 0x05, 0x61, 0x74, 0x74, 0x72, 0x73, 0x22, 0x83, - 0x02, 0x0a, 0x1c, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x41, 0x75, 0x74, 0x68, 0x4d, - 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, - 0x74, 0x0a, 0x15, 0x6d, 0x69, 0x6e, 0x5f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, - 0x65, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x3e, - 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x36, 0x0a, 0x20, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x73, 0x2e, 0x6d, 0x69, 0x6e, 0x5f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x6e, - 0x61, 0x6d, 0x65, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x12, 0x12, 0x4d, 0x69, 0x6e, 0x4c, - 0x6f, 0x67, 0x69, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x52, 0x15, - 0x6d, 0x69, 0x6e, 0x5f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x6c, - 0x65, 0x6e, 0x67, 0x74, 0x68, 0x12, 0x6d, 0x0a, 0x13, 0x6d, 0x69, 0x6e, 0x5f, 0x70, 0x61, 0x73, - 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x14, 0x20, 0x01, - 0x28, 0x0d, 0x42, 0x3b, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x33, 0x0a, 0x1e, 0x61, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x6d, 0x69, 0x6e, 0x5f, 0x70, 0x61, 0x73, - 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x12, 0x11, 0x4d, 0x69, - 0x6e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x52, - 0x13, 0x6d, 0x69, 0x6e, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x6c, 0x65, - 0x6e, 0x67, 0x74, 0x68, 0x22, 0xe6, 0x09, 0x0a, 0x18, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, - 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x73, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x59, 0x0a, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, - 0x72, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x2a, 0xa0, 0xda, + 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x22, 0x0a, 0x15, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x73, 0x2e, 0x75, 0x70, 0x6e, 0x5f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x09, 0x55, + 0x70, 0x6e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x0a, 0x75, 0x70, 0x6e, 0x5f, 0x64, 0x6f, + 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x33, 0x0a, 0x04, 0x75, 0x72, 0x6c, 0x73, 0x18, 0x46, 0x20, 0x03, + 0x28, 0x09, 0x42, 0x1f, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x17, 0x0a, 0x0f, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x75, 0x72, 0x6c, 0x73, 0x12, 0x04, 0x55, + 0x72, 0x6c, 0x73, 0x52, 0x04, 0x75, 0x72, 0x6c, 0x73, 0x12, 0x5c, 0x0a, 0x07, 0x75, 0x73, 0x65, + 0x72, 0x5f, 0x64, 0x6e, 0x18, 0x50, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, + 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x24, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, + 0x29, 0x1c, 0x0a, 0x12, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x75, + 0x73, 0x65, 0x72, 0x5f, 0x64, 0x6e, 0x12, 0x06, 0x55, 0x73, 0x65, 0x72, 0x44, 0x6e, 0x52, 0x07, + 0x75, 0x73, 0x65, 0x72, 0x5f, 0x64, 0x6e, 0x12, 0x64, 0x0a, 0x09, 0x75, 0x73, 0x65, 0x72, 0x5f, + 0x61, 0x74, 0x74, 0x72, 0x18, 0x5a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, + 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x28, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, + 0x29, 0x20, 0x0a, 0x14, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x75, + 0x73, 0x65, 0x72, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x12, 0x08, 0x55, 0x73, 0x65, 0x72, 0x41, 0x74, + 0x74, 0x72, 0x52, 0x09, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x12, 0x6c, 0x0a, + 0x0b, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x64, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x42, 0x2c, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x24, 0x0a, 0x16, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x66, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x12, 0x0a, 0x55, 0x73, 0x65, 0x72, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x0b, + 0x75, 0x73, 0x65, 0x72, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x56, 0x0a, 0x0d, 0x65, + 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x6e, 0x20, 0x01, + 0x28, 0x08, 0x42, 0x30, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x28, 0x0a, 0x18, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x5f, + 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x0c, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x47, 0x72, + 0x6f, 0x75, 0x70, 0x73, 0x52, 0x0d, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x67, 0x72, 0x6f, + 0x75, 0x70, 0x73, 0x12, 0x60, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x64, 0x6e, 0x18, + 0x78, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x42, 0x26, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1e, 0x0a, 0x13, 0x61, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, + 0x64, 0x6e, 0x12, 0x07, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x44, 0x6e, 0x52, 0x08, 0x67, 0x72, 0x6f, + 0x75, 0x70, 0x5f, 0x64, 0x6e, 0x12, 0x69, 0x0a, 0x0a, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x61, + 0x74, 0x74, 0x72, 0x18, 0x82, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, + 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x2a, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, + 0x29, 0x22, 0x0a, 0x15, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x67, + 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x12, 0x09, 0x47, 0x72, 0x6f, 0x75, 0x70, + 0x41, 0x74, 0x74, 0x72, 0x52, 0x0a, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x61, 0x74, 0x74, 0x72, + 0x12, 0x71, 0x0a, 0x0c, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x18, 0x8c, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x2e, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x26, 0x0a, + 0x17, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x67, 0x72, 0x6f, 0x75, + 0x70, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x0b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x46, + 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x0c, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x66, 0x69, 0x6c, + 0x74, 0x65, 0x72, 0x12, 0x54, 0x0a, 0x0c, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x73, 0x18, 0x96, 0x01, 0x20, 0x03, 0x28, 0x09, 0x42, 0x2f, 0xa0, 0xda, 0x29, 0x01, + 0xc2, 0xdd, 0x29, 0x27, 0x0a, 0x17, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, + 0x2e, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x12, 0x0c, 0x43, + 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x52, 0x0c, 0x63, 0x65, 0x72, + 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x12, 0x89, 0x01, 0x0a, 0x12, 0x63, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, + 0x18, 0xa0, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x3a, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x32, 0x0a, + 0x1d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x63, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x11, + 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x52, 0x12, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x98, 0x01, 0x0a, 0x16, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, + 0x18, 0xaa, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x23, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1b, 0x0a, - 0x11, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x69, 0x73, 0x73, 0x75, - 0x65, 0x72, 0x12, 0x06, 0x49, 0x73, 0x73, 0x75, 0x65, 0x72, 0x52, 0x06, 0x69, 0x73, 0x73, 0x75, - 0x65, 0x72, 0x12, 0x64, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, - 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x41, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x39, 0x0a, + 0x21, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x63, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x6b, + 0x65, 0x79, 0x12, 0x14, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x16, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, + 0x12, 0x41, 0x0a, 0x1b, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x18, + 0xb4, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1b, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x63, + 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x68, + 0x6d, 0x61, 0x63, 0x12, 0x5d, 0x0a, 0x07, 0x62, 0x69, 0x6e, 0x64, 0x5f, 0x64, 0x6e, 0x18, 0xbe, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x42, 0x28, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x20, 0x0a, 0x14, 0x61, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, - 0x5f, 0x69, 0x64, 0x12, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x52, 0x09, 0x63, - 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x12, 0x74, 0x0a, 0x0d, 0x63, 0x6c, 0x69, 0x65, - 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x28, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x30, 0xa0, - 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x28, 0x0a, 0x18, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x65, 0x73, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, - 0x74, 0x12, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, - 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x2e, - 0x0a, 0x12, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x5f, - 0x68, 0x6d, 0x61, 0x63, 0x18, 0x32, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x63, 0x6c, 0x69, 0x65, - 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x12, 0x5c, - 0x0a, 0x07, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x67, 0x65, 0x18, 0x3c, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x55, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x24, 0xa0, - 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1c, 0x0a, 0x12, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x65, 0x73, 0x2e, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x67, 0x65, 0x12, 0x06, 0x4d, 0x61, 0x78, - 0x41, 0x67, 0x65, 0x52, 0x07, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x67, 0x65, 0x12, 0x64, 0x0a, 0x12, - 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, - 0x6d, 0x73, 0x18, 0x46, 0x20, 0x03, 0x28, 0x09, 0x42, 0x34, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, - 0x29, 0x2c, 0x0a, 0x1d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x73, - 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, - 0x73, 0x12, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x41, 0x6c, 0x67, 0x73, 0x52, 0x12, - 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, - 0x6d, 0x73, 0x12, 0x71, 0x0a, 0x0e, 0x61, 0x70, 0x69, 0x5f, 0x75, 0x72, 0x6c, 0x5f, 0x70, 0x72, - 0x65, 0x66, 0x69, 0x78, 0x18, 0x50, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, + 0x6c, 0x75, 0x65, 0x42, 0x24, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1c, 0x0a, 0x12, 0x61, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x62, 0x69, 0x6e, 0x64, 0x5f, 0x64, + 0x6e, 0x12, 0x06, 0x42, 0x69, 0x6e, 0x64, 0x44, 0x6e, 0x52, 0x07, 0x62, 0x69, 0x6e, 0x64, 0x5f, + 0x64, 0x6e, 0x12, 0x75, 0x0a, 0x0d, 0x62, 0x69, 0x6e, 0x64, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, + 0x6f, 0x72, 0x64, 0x18, 0xc8, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, - 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x2b, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, - 0x29, 0x23, 0x0a, 0x19, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x61, - 0x70, 0x69, 0x5f, 0x75, 0x72, 0x6c, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x06, 0x41, - 0x70, 0x69, 0x55, 0x72, 0x6c, 0x52, 0x0e, 0x61, 0x70, 0x69, 0x5f, 0x75, 0x72, 0x6c, 0x5f, 0x70, - 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x22, 0x0a, 0x0c, 0x63, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, - 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x5a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x61, 0x6c, - 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x12, 0x53, 0x0a, 0x0c, 0x69, 0x64, 0x70, - 0x5f, 0x63, 0x61, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x73, 0x18, 0x64, 0x20, 0x03, 0x28, 0x09, 0x42, - 0x2f, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x27, 0x0a, 0x17, 0x61, 0x74, 0x74, 0x72, 0x69, - 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x69, 0x64, 0x70, 0x5f, 0x63, 0x61, 0x5f, 0x63, 0x65, 0x72, - 0x74, 0x73, 0x12, 0x0c, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, - 0x52, 0x0c, 0x69, 0x64, 0x70, 0x5f, 0x63, 0x61, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x73, 0x12, 0x5f, - 0x0a, 0x11, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, - 0x63, 0x65, 0x73, 0x18, 0x6e, 0x20, 0x03, 0x28, 0x09, 0x42, 0x31, 0xa0, 0xda, 0x29, 0x01, 0xc2, - 0xdd, 0x29, 0x29, 0x0a, 0x1c, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, - 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, - 0x73, 0x12, 0x09, 0x41, 0x75, 0x64, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x52, 0x11, 0x61, 0x6c, - 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x12, - 0x56, 0x0a, 0x0d, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, - 0x18, 0x70, 0x20, 0x03, 0x28, 0x09, 0x42, 0x30, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x28, - 0x0a, 0x18, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x63, 0x6c, 0x61, - 0x69, 0x6d, 0x73, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x12, 0x0c, 0x43, 0x6c, 0x61, 0x69, - 0x6d, 0x73, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x52, 0x0d, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x73, - 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x12, 0x69, 0x0a, 0x12, 0x61, 0x63, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x5f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x5f, 0x6d, 0x61, 0x70, 0x73, 0x18, 0x71, 0x20, - 0x03, 0x28, 0x09, 0x42, 0x39, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x31, 0x0a, 0x1d, 0x61, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x5f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x5f, 0x6d, 0x61, 0x70, 0x73, 0x12, 0x10, 0x41, 0x63, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x4d, 0x61, 0x70, 0x73, 0x52, 0x12, - 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x5f, 0x6d, 0x61, - 0x70, 0x73, 0x12, 0x58, 0x0a, 0x24, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x69, - 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, - 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x78, 0x20, 0x01, 0x28, 0x08, - 0x42, 0x04, 0xa0, 0xda, 0x29, 0x01, 0x52, 0x24, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, - 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x07, - 0x64, 0x72, 0x79, 0x5f, 0x72, 0x75, 0x6e, 0x18, 0x82, 0x01, 0x20, 0x01, 0x28, 0x08, 0x42, 0x04, - 0xa0, 0xda, 0x29, 0x01, 0x52, 0x07, 0x64, 0x72, 0x79, 0x5f, 0x72, 0x75, 0x6e, 0x22, 0x61, 0x0a, - 0x27, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, - 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x75, 0x74, 0x68, - 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x75, 0x74, 0x68, - 0x5f, 0x75, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x69, 0x64, - 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x69, 0x64, - 0x22, 0xb7, 0x01, 0x0a, 0x29, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, - 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, - 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, - 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, - 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, - 0x72, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x2c, - 0x0a, 0x11, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, - 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x32, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x75, 0x72, 0x69, 0x22, 0x5c, 0x0a, 0x2a, 0x4f, 0x69, - 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, - 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x66, 0x69, 0x6e, 0x61, - 0x6c, 0x5f, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x0a, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x72, 0x65, 0x64, 0x69, - 0x72, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x72, 0x6c, 0x22, 0x44, 0x0a, 0x26, 0x4f, 0x69, 0x64, 0x63, - 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, - 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0a, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x69, 0x64, 0x22, 0x41, - 0x0a, 0x27, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, - 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x42, 0x60, 0x5a, 0x56, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, - 0x72, 0x79, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, - 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x73, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x3b, - 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0xa2, 0xe3, 0x29, 0x04, 0x61, - 0x75, 0x74, 0x68, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x30, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, + 0x29, 0x28, 0x0a, 0x18, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x62, + 0x69, 0x6e, 0x64, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x0c, 0x42, 0x69, + 0x6e, 0x64, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x0d, 0x62, 0x69, 0x6e, 0x64, + 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x2f, 0x0a, 0x12, 0x62, 0x69, 0x6e, + 0x64, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x18, + 0xd2, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x62, 0x69, 0x6e, 0x64, 0x5f, 0x70, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x12, 0x62, 0x0a, 0x10, 0x75, 0x73, + 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0xdc, + 0x01, 0x20, 0x01, 0x28, 0x08, 0x42, 0x35, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x2d, 0x0a, + 0x1b, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x75, 0x73, 0x65, 0x5f, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x0e, 0x55, 0x73, + 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x52, 0x10, 0x75, 0x73, + 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x7a, + 0x0a, 0x16, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x70, 0x73, 0x18, 0xe6, 0x01, 0x20, 0x03, 0x28, 0x09, 0x42, + 0x41, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x39, 0x0a, 0x21, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x70, 0x73, 0x12, 0x14, 0x41, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4d, 0x61, + 0x70, 0x73, 0x52, 0x16, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x70, 0x73, 0x42, 0x60, 0x5a, 0x56, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, + 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x73, 0x64, 0x6b, 0x2f, + 0x70, 0x62, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, + 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x61, 0x75, 0x74, + 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x3b, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, + 0x68, 0x6f, 0x64, 0x73, 0xa2, 0xe3, 0x29, 0x04, 0x61, 0x75, 0x74, 0x68, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1036,7 +1492,7 @@ func file_controller_api_resources_authmethods_v1_auth_method_proto_rawDescGZIP( return file_controller_api_resources_authmethods_v1_auth_method_proto_rawDescData } -var file_controller_api_resources_authmethods_v1_auth_method_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_controller_api_resources_authmethods_v1_auth_method_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_controller_api_resources_authmethods_v1_auth_method_proto_goTypes = []interface{}{ (*AuthMethod)(nil), // 0: controller.api.resources.authmethods.v1.AuthMethod (*PasswordAuthMethodAttributes)(nil), // 1: controller.api.resources.authmethods.v1.PasswordAuthMethodAttributes @@ -1046,35 +1502,48 @@ var file_controller_api_resources_authmethods_v1_auth_method_proto_goTypes = []i (*OidcAuthMethodAuthenticateCallbackResponse)(nil), // 5: controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateCallbackResponse (*OidcAuthMethodAuthenticateTokenRequest)(nil), // 6: controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateTokenRequest (*OidcAuthMethodAuthenticateTokenResponse)(nil), // 7: controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateTokenResponse - nil, // 8: controller.api.resources.authmethods.v1.AuthMethod.AuthorizedCollectionActionsEntry - (*scopes.ScopeInfo)(nil), // 9: controller.api.resources.scopes.v1.ScopeInfo - (*wrapperspb.StringValue)(nil), // 10: google.protobuf.StringValue - (*timestamppb.Timestamp)(nil), // 11: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 12: google.protobuf.Struct - (*wrapperspb.UInt32Value)(nil), // 13: google.protobuf.UInt32Value - (*structpb.ListValue)(nil), // 14: google.protobuf.ListValue + (*LdapAuthMethodAttributes)(nil), // 8: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes + nil, // 9: controller.api.resources.authmethods.v1.AuthMethod.AuthorizedCollectionActionsEntry + (*scopes.ScopeInfo)(nil), // 10: controller.api.resources.scopes.v1.ScopeInfo + (*wrapperspb.StringValue)(nil), // 11: google.protobuf.StringValue + (*timestamppb.Timestamp)(nil), // 12: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 13: google.protobuf.Struct + (*wrapperspb.UInt32Value)(nil), // 14: google.protobuf.UInt32Value + (*structpb.ListValue)(nil), // 15: google.protobuf.ListValue } var file_controller_api_resources_authmethods_v1_auth_method_proto_depIdxs = []int32{ - 9, // 0: controller.api.resources.authmethods.v1.AuthMethod.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo - 10, // 1: controller.api.resources.authmethods.v1.AuthMethod.name:type_name -> google.protobuf.StringValue - 10, // 2: controller.api.resources.authmethods.v1.AuthMethod.description:type_name -> google.protobuf.StringValue - 11, // 3: controller.api.resources.authmethods.v1.AuthMethod.created_time:type_name -> google.protobuf.Timestamp - 11, // 4: controller.api.resources.authmethods.v1.AuthMethod.updated_time:type_name -> google.protobuf.Timestamp - 12, // 5: controller.api.resources.authmethods.v1.AuthMethod.attributes:type_name -> google.protobuf.Struct + 10, // 0: controller.api.resources.authmethods.v1.AuthMethod.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo + 11, // 1: controller.api.resources.authmethods.v1.AuthMethod.name:type_name -> google.protobuf.StringValue + 11, // 2: controller.api.resources.authmethods.v1.AuthMethod.description:type_name -> google.protobuf.StringValue + 12, // 3: controller.api.resources.authmethods.v1.AuthMethod.created_time:type_name -> google.protobuf.Timestamp + 12, // 4: controller.api.resources.authmethods.v1.AuthMethod.updated_time:type_name -> google.protobuf.Timestamp + 13, // 5: controller.api.resources.authmethods.v1.AuthMethod.attributes:type_name -> google.protobuf.Struct 1, // 6: controller.api.resources.authmethods.v1.AuthMethod.password_auth_method_attributes:type_name -> controller.api.resources.authmethods.v1.PasswordAuthMethodAttributes 2, // 7: controller.api.resources.authmethods.v1.AuthMethod.oidc_auth_methods_attributes:type_name -> controller.api.resources.authmethods.v1.OidcAuthMethodAttributes - 8, // 8: controller.api.resources.authmethods.v1.AuthMethod.authorized_collection_actions:type_name -> controller.api.resources.authmethods.v1.AuthMethod.AuthorizedCollectionActionsEntry - 10, // 9: controller.api.resources.authmethods.v1.OidcAuthMethodAttributes.issuer:type_name -> google.protobuf.StringValue - 10, // 10: controller.api.resources.authmethods.v1.OidcAuthMethodAttributes.client_id:type_name -> google.protobuf.StringValue - 10, // 11: controller.api.resources.authmethods.v1.OidcAuthMethodAttributes.client_secret:type_name -> google.protobuf.StringValue - 13, // 12: controller.api.resources.authmethods.v1.OidcAuthMethodAttributes.max_age:type_name -> google.protobuf.UInt32Value - 10, // 13: controller.api.resources.authmethods.v1.OidcAuthMethodAttributes.api_url_prefix:type_name -> google.protobuf.StringValue - 14, // 14: controller.api.resources.authmethods.v1.AuthMethod.AuthorizedCollectionActionsEntry.value:type_name -> google.protobuf.ListValue - 15, // [15:15] is the sub-list for method output_type - 15, // [15:15] is the sub-list for method input_type - 15, // [15:15] is the sub-list for extension type_name - 15, // [15:15] is the sub-list for extension extendee - 0, // [0:15] is the sub-list for field type_name + 8, // 8: controller.api.resources.authmethods.v1.AuthMethod.ldap_auth_methods_attributes:type_name -> controller.api.resources.authmethods.v1.LdapAuthMethodAttributes + 9, // 9: controller.api.resources.authmethods.v1.AuthMethod.authorized_collection_actions:type_name -> controller.api.resources.authmethods.v1.AuthMethod.AuthorizedCollectionActionsEntry + 11, // 10: controller.api.resources.authmethods.v1.OidcAuthMethodAttributes.issuer:type_name -> google.protobuf.StringValue + 11, // 11: controller.api.resources.authmethods.v1.OidcAuthMethodAttributes.client_id:type_name -> google.protobuf.StringValue + 11, // 12: controller.api.resources.authmethods.v1.OidcAuthMethodAttributes.client_secret:type_name -> google.protobuf.StringValue + 14, // 13: controller.api.resources.authmethods.v1.OidcAuthMethodAttributes.max_age:type_name -> google.protobuf.UInt32Value + 11, // 14: controller.api.resources.authmethods.v1.OidcAuthMethodAttributes.api_url_prefix:type_name -> google.protobuf.StringValue + 11, // 15: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.upn_domain:type_name -> google.protobuf.StringValue + 11, // 16: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.user_dn:type_name -> google.protobuf.StringValue + 11, // 17: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.user_attr:type_name -> google.protobuf.StringValue + 11, // 18: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.user_filter:type_name -> google.protobuf.StringValue + 11, // 19: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.group_dn:type_name -> google.protobuf.StringValue + 11, // 20: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.group_attr:type_name -> google.protobuf.StringValue + 11, // 21: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.group_filter:type_name -> google.protobuf.StringValue + 11, // 22: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.client_certificate:type_name -> google.protobuf.StringValue + 11, // 23: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.client_certificate_key:type_name -> google.protobuf.StringValue + 11, // 24: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.bind_dn:type_name -> google.protobuf.StringValue + 11, // 25: controller.api.resources.authmethods.v1.LdapAuthMethodAttributes.bind_password:type_name -> google.protobuf.StringValue + 15, // 26: controller.api.resources.authmethods.v1.AuthMethod.AuthorizedCollectionActionsEntry.value:type_name -> google.protobuf.ListValue + 27, // [27:27] is the sub-list for method output_type + 27, // [27:27] is the sub-list for method input_type + 27, // [27:27] is the sub-list for extension type_name + 27, // [27:27] is the sub-list for extension extendee + 0, // [0:27] is the sub-list for field type_name } func init() { file_controller_api_resources_authmethods_v1_auth_method_proto_init() } @@ -1179,11 +1648,24 @@ func file_controller_api_resources_authmethods_v1_auth_method_proto_init() { return nil } } + file_controller_api_resources_authmethods_v1_auth_method_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LdapAuthMethodAttributes); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_controller_api_resources_authmethods_v1_auth_method_proto_msgTypes[0].OneofWrappers = []interface{}{ (*AuthMethod_Attributes)(nil), (*AuthMethod_PasswordAuthMethodAttributes)(nil), (*AuthMethod_OidcAuthMethodsAttributes)(nil), + (*AuthMethod_LdapAuthMethodsAttributes)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -1191,7 +1673,7 @@ func file_controller_api_resources_authmethods_v1_auth_method_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_api_resources_authmethods_v1_auth_method_proto_rawDesc, NumEnums: 0, - NumMessages: 9, + NumMessages: 10, NumExtensions: 0, NumServices: 0, }, diff --git a/sdk/pbs/controller/api/resources/managedgroups/managed_group.pb.go b/sdk/pbs/controller/api/resources/managedgroups/managed_group.pb.go index 0e2a5e86f0..73975ee26a 100644 --- a/sdk/pbs/controller/api/resources/managedgroups/managed_group.pb.go +++ b/sdk/pbs/controller/api/resources/managedgroups/managed_group.pb.go @@ -58,6 +58,7 @@ type ManagedGroup struct { // // *ManagedGroup_Attributes // *ManagedGroup_OidcManagedGroupAttributes + // *ManagedGroup_LdapManagedGroupAttributes Attrs isManagedGroup_Attrs `protobuf_oneof:"attrs"` // Output only. The IDs of the current set of members (accounts) that are associated with this ManagedGroup. MemberIds []string `protobuf:"bytes,110,rep,name=member_ids,proto3" json:"member_ids,omitempty" class:"public"` // @gotags: `class:"public"` @@ -181,6 +182,13 @@ func (x *ManagedGroup) GetOidcManagedGroupAttributes() *OidcManagedGroupAttribut return nil } +func (x *ManagedGroup) GetLdapManagedGroupAttributes() *LdapManagedGroupAttributes { + if x, ok := x.GetAttrs().(*ManagedGroup_LdapManagedGroupAttributes); ok { + return x.LdapManagedGroupAttributes + } + return nil +} + func (x *ManagedGroup) GetMemberIds() []string { if x != nil { return x.MemberIds @@ -208,10 +216,16 @@ type ManagedGroup_OidcManagedGroupAttributes struct { OidcManagedGroupAttributes *OidcManagedGroupAttributes `protobuf:"bytes,101,opt,name=oidc_managed_group_attributes,json=oidcManagedGroupAttributes,proto3,oneof"` } +type ManagedGroup_LdapManagedGroupAttributes struct { + LdapManagedGroupAttributes *LdapManagedGroupAttributes `protobuf:"bytes,102,opt,name=ldap_managed_group_attributes,json=ldapManagedGroupAttributes,proto3,oneof"` +} + func (*ManagedGroup_Attributes) isManagedGroup_Attrs() {} func (*ManagedGroup_OidcManagedGroupAttributes) isManagedGroup_Attrs() {} +func (*ManagedGroup_LdapManagedGroupAttributes) isManagedGroup_Attrs() {} + // Attributes associated only with ManagedGroups with type "oidc". type OidcManagedGroupAttributes struct { state protoimpl.MessageState @@ -261,6 +275,55 @@ func (x *OidcManagedGroupAttributes) GetFilter() string { return "" } +// Attributes associated only with ManagedGroups with type "ldap". +type LdapManagedGroupAttributes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The list of groups that make up the ManagedGroup + GroupNames []string `protobuf:"bytes,100,rep,name=group_names,proto3" json:"group_names,omitempty" class:"public"` // @gotags: `class:"public"` +} + +func (x *LdapManagedGroupAttributes) Reset() { + *x = LdapManagedGroupAttributes{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_resources_managedgroups_v1_managed_group_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LdapManagedGroupAttributes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LdapManagedGroupAttributes) ProtoMessage() {} + +func (x *LdapManagedGroupAttributes) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_resources_managedgroups_v1_managed_group_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LdapManagedGroupAttributes.ProtoReflect.Descriptor instead. +func (*LdapManagedGroupAttributes) Descriptor() ([]byte, []int) { + return file_controller_api_resources_managedgroups_v1_managed_group_proto_rawDescGZIP(), []int{2} +} + +func (x *LdapManagedGroupAttributes) GetGroupNames() []string { + if x != nil { + return x.GroupNames + } + return nil +} + var File_controller_api_resources_managedgroups_v1_managed_group_proto protoreflect.FileDescriptor var file_controller_api_resources_managedgroups_v1_managed_group_proto_rawDesc = []byte{ @@ -284,7 +347,7 @@ var file_controller_api_resources_managedgroups_v1_managed_group_proto_rawDesc = 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x22, 0xbc, 0x06, 0x0a, 0x0c, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, + 0x74, 0x6f, 0x22, 0xe7, 0x07, 0x0a, 0x0c, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x43, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, @@ -330,25 +393,42 @@ var file_controller_api_resources_managedgroups_v1_managed_group_proto_rawDesc = 0x01, 0x9a, 0xe3, 0x29, 0x04, 0x6f, 0x69, 0x64, 0x63, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x1a, 0x6f, 0x69, 0x64, 0x63, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x41, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x6d, 0x65, 0x6d, 0x62, 0x65, - 0x72, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x6e, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x65, 0x6d, - 0x62, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x73, 0x12, 0x2f, 0x0a, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xac, 0x02, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, - 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x07, 0x0a, 0x05, 0x61, 0x74, 0x74, 0x72, - 0x73, 0x22, 0x59, 0x0a, 0x1a, 0x4f, 0x69, 0x64, 0x63, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, - 0x47, 0x72, 0x6f, 0x75, 0x70, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, - 0x3b, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x42, - 0x23, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1b, 0x0a, 0x11, 0x61, 0x74, 0x74, 0x72, 0x69, - 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x06, 0x46, 0x69, - 0x6c, 0x74, 0x65, 0x72, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x42, 0x5c, 0x5a, 0x5a, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, - 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x73, 0x64, - 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, - 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x3b, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x64, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0xa8, 0x01, 0x0a, 0x1d, 0x6c, 0x64, 0x61, 0x70, + 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x61, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x66, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x45, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x64, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x64, 0x61, 0x70, + 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x41, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x42, 0x1c, 0xa0, 0xda, 0x29, 0x01, 0x9a, 0xe3, 0x29, 0x04, + 0x6c, 0x64, 0x61, 0x70, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, + 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x1a, 0x6c, 0x64, 0x61, 0x70, 0x4d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x73, + 0x18, 0x6e, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x5f, 0x69, + 0x64, 0x73, 0x12, 0x2f, 0x0a, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, + 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xac, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x42, 0x07, 0x0a, 0x05, 0x61, 0x74, 0x74, 0x72, 0x73, 0x22, 0x59, 0x0a, 0x1a, + 0x4f, 0x69, 0x64, 0x63, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, + 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x06, 0x66, 0x69, + 0x6c, 0x74, 0x65, 0x72, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x42, 0x23, 0xa0, 0xda, 0x29, 0x01, + 0xc2, 0xdd, 0x29, 0x1b, 0x0a, 0x11, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, + 0x2e, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x06, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, + 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x6c, 0x0a, 0x1a, 0x4c, 0x64, 0x61, 0x70, 0x4d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x41, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x4e, 0x0a, 0x0b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x6e, + 0x61, 0x6d, 0x65, 0x73, 0x18, 0x64, 0x20, 0x03, 0x28, 0x09, 0x42, 0x2c, 0xa0, 0xda, 0x29, 0x01, + 0xc2, 0xdd, 0x29, 0x24, 0x0a, 0x16, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, + 0x2e, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x0a, 0x47, 0x72, + 0x6f, 0x75, 0x70, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x52, 0x0b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x42, 0x5c, 0x5a, 0x5a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, + 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x63, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x67, + 0x72, 0x6f, 0x75, 0x70, 0x73, 0x3b, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x67, 0x72, 0x6f, + 0x75, 0x70, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -363,28 +443,30 @@ func file_controller_api_resources_managedgroups_v1_managed_group_proto_rawDescG return file_controller_api_resources_managedgroups_v1_managed_group_proto_rawDescData } -var file_controller_api_resources_managedgroups_v1_managed_group_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_controller_api_resources_managedgroups_v1_managed_group_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file_controller_api_resources_managedgroups_v1_managed_group_proto_goTypes = []interface{}{ (*ManagedGroup)(nil), // 0: controller.api.resources.managedgroups.v1.ManagedGroup (*OidcManagedGroupAttributes)(nil), // 1: controller.api.resources.managedgroups.v1.OidcManagedGroupAttributes - (*scopes.ScopeInfo)(nil), // 2: controller.api.resources.scopes.v1.ScopeInfo - (*wrapperspb.StringValue)(nil), // 3: google.protobuf.StringValue - (*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 5: google.protobuf.Struct + (*LdapManagedGroupAttributes)(nil), // 2: controller.api.resources.managedgroups.v1.LdapManagedGroupAttributes + (*scopes.ScopeInfo)(nil), // 3: controller.api.resources.scopes.v1.ScopeInfo + (*wrapperspb.StringValue)(nil), // 4: google.protobuf.StringValue + (*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 6: google.protobuf.Struct } var file_controller_api_resources_managedgroups_v1_managed_group_proto_depIdxs = []int32{ - 2, // 0: controller.api.resources.managedgroups.v1.ManagedGroup.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo - 3, // 1: controller.api.resources.managedgroups.v1.ManagedGroup.name:type_name -> google.protobuf.StringValue - 3, // 2: controller.api.resources.managedgroups.v1.ManagedGroup.description:type_name -> google.protobuf.StringValue - 4, // 3: controller.api.resources.managedgroups.v1.ManagedGroup.created_time:type_name -> google.protobuf.Timestamp - 4, // 4: controller.api.resources.managedgroups.v1.ManagedGroup.updated_time:type_name -> google.protobuf.Timestamp - 5, // 5: controller.api.resources.managedgroups.v1.ManagedGroup.attributes:type_name -> google.protobuf.Struct + 3, // 0: controller.api.resources.managedgroups.v1.ManagedGroup.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo + 4, // 1: controller.api.resources.managedgroups.v1.ManagedGroup.name:type_name -> google.protobuf.StringValue + 4, // 2: controller.api.resources.managedgroups.v1.ManagedGroup.description:type_name -> google.protobuf.StringValue + 5, // 3: controller.api.resources.managedgroups.v1.ManagedGroup.created_time:type_name -> google.protobuf.Timestamp + 5, // 4: controller.api.resources.managedgroups.v1.ManagedGroup.updated_time:type_name -> google.protobuf.Timestamp + 6, // 5: controller.api.resources.managedgroups.v1.ManagedGroup.attributes:type_name -> google.protobuf.Struct 1, // 6: controller.api.resources.managedgroups.v1.ManagedGroup.oidc_managed_group_attributes:type_name -> controller.api.resources.managedgroups.v1.OidcManagedGroupAttributes - 7, // [7:7] is the sub-list for method output_type - 7, // [7:7] is the sub-list for method input_type - 7, // [7:7] is the sub-list for extension type_name - 7, // [7:7] is the sub-list for extension extendee - 0, // [0:7] is the sub-list for field type_name + 2, // 7: controller.api.resources.managedgroups.v1.ManagedGroup.ldap_managed_group_attributes:type_name -> controller.api.resources.managedgroups.v1.LdapManagedGroupAttributes + 8, // [8:8] is the sub-list for method output_type + 8, // [8:8] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name } func init() { file_controller_api_resources_managedgroups_v1_managed_group_proto_init() } @@ -417,10 +499,23 @@ func file_controller_api_resources_managedgroups_v1_managed_group_proto_init() { return nil } } + file_controller_api_resources_managedgroups_v1_managed_group_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LdapManagedGroupAttributes); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_controller_api_resources_managedgroups_v1_managed_group_proto_msgTypes[0].OneofWrappers = []interface{}{ (*ManagedGroup_Attributes)(nil), (*ManagedGroup_OidcManagedGroupAttributes)(nil), + (*ManagedGroup_LdapManagedGroupAttributes)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -428,7 +523,7 @@ func file_controller_api_resources_managedgroups_v1_managed_group_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_api_resources_managedgroups_v1_managed_group_proto_rawDesc, NumEnums: 0, - NumMessages: 2, + NumMessages: 3, NumExtensions: 0, NumServices: 0, }, diff --git a/website/content/docs/concepts/domain-model/accounts.mdx b/website/content/docs/concepts/domain-model/accounts.mdx index 4570b297b6..f47126468f 100644 --- a/website/content/docs/concepts/domain-model/accounts.mdx +++ b/website/content/docs/concepts/domain-model/accounts.mdx @@ -36,6 +36,33 @@ Password account types have the following additional attributes: - `password` - (optional) Not setting the `password` disables the account. +### LDAP Account Attributes + +LDAP account types have the following additional attributes: + +- `login_name` - (required) + Must be unique within the account's [auth method][]. + Can only contain lower case letters. + +- `full_name` - (output only) + Maps to the name attribute for the authenticated user, and is updated every + time the user successfully authenticates. It is empty until the user's + first successful authentication. + +- `email` - (output only) + Maps to the email address attribute for the authenticated user, and is updated + every time the user successfully authenticates. It is empty until the + user's first successful authentication. + +- `dn` - (output only) + Maps to the distinguished name for the authenticated user, and is updated + every time the user successfully authenticates. It is empty until the + user's first successful authentication. + +- `member_of_groups` - (output only) + A list of the groups the authenticated user is a member of. It is empty + until the user's first successful authentication. + ## Referenced By - [Auth Method][] diff --git a/website/content/docs/concepts/domain-model/auth-methods.mdx b/website/content/docs/concepts/domain-model/auth-methods.mdx index f92f30ada4..191f40bc03 100644 --- a/website/content/docs/concepts/domain-model/auth-methods.mdx +++ b/website/content/docs/concepts/domain-model/auth-methods.mdx @@ -31,6 +31,98 @@ The password auth method has the following additional attributes: - `min_password_length` - (required) The default is 8. +### LDAP Auth Method Attributes + +The ldap auth method has the following additional attributes: + +- `state` - The state of the auth method; either inactive, active-private, or + active-public. + +- `start_tls` - (optional) If true, issues a StartTLS command after establishing + an unencrypted connection. Defaults to false. + +- `insecure_tls` - (optional) If true, skips LDAP server SSL certificate + validation, which is insecure and should be used with caution. Defaults to + false. + +- `discover_dn` - (optional) If true, use anon bind to discover the bind DN + (Distinguished Name) of a user. Defaults to false. + +- `anon_group_search` - (optional) If true, use anon bind when performing LDAP + group searches. Defaults to false. + +- `upn_domain` - (optional) If set, the userPrincipalDomain is used to construct + the UPN string for the authenticating user. The constructed UPN appears as + [username]@UPNDomain Example: example.com, which causes Boundary to + bind as username@example.com when it authenticates the user. + +- `urls` - (required) The LDAP URLS that specify LDAP servers to connect to. + There must be at least one URL for each LDAP auth method. When attempting to + connect, the URLs are tried in the order specified. + +- `user_dn` - (optional) If set, the base DN under which to perform user + search. Example: ou=Users,dc=example,dc=com + +- `user_attr` - (optional) If set, defines the attribute on a user's entry + matching the login-name passed when the user authenticates. Examples: cn, uid + +- `user_filter` - (optional) If set, the Go template used to construct an LDAP + user search filter. The template can access the following context variables: + [UserAttr, Username]. The default user_filter is ({{.UserAttr}}={{.Username}}) + or (userPrincipalName={{.Username}}@UPNDomain) if the upn-domain parameter is + set. + +- `enable_groups` - (optional) If true, an authenticated user's groups are + found during authentication. Defaults to false. + +- `group_dn` - (optional) If set, the base DN under which to perform a group + search. Example: ou=Groups,dc=example,dc=com + + Note: There is no default, so no base DN is used for group searches, if + it's not specified. + +- `group_attr` - (optional) If set, the LDAP attribute to follow on objects + returned by group_filter in order to enumerate user group membership. + Examples: for group_filter queries returning group objects, use: cn. For + queries returning user objects, use: memberOf. The default is cn. + +- `group_filter` - (optional) If set, the Go template used when constructing the + group membership query. The template can access the following context + variables: [UserDN, Username]. The default is + (|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}})), + which is compatible with several common directory schemas. + +- `certificates` - (optional) If set, PEM encoded x509 certificates in ASN.1 + DER form that can be used as trust anchors when connecting to an LDAP + provider. + +- `client_certificate` - (optional) If set, a PEM encoded x509 certificate in + ASN.1 DER form to be used as a client certificate. It must be set, if you + specify the optional client_certificate_key. + +- `client_certificate_key` - (optional) If set, a PEM encoded certificate key in + PKCS #8, ASN.1 DER form. It must be set, if you specify the optional + client_certificate. + +- `bind_dn` - (optional) If set, the distinguished name of entry to bind when + performing user and group searches. Example: + cn=vault,ou=Users,dc=example,dc=com + +- `bind_password` - (optional) If set, the password to use along with bind_dn + when performing user search. It must be set, if you specify the optional + bind_dn. + +- `use_token_groups` - (optional) If true, use the Active Directory tokenGroups + constructed attribute of the user to find the group memberships. This + finds all security groups, including nested ones. + +- `account_attribute_maps` - (optional) If set, the attribute maps from custom + attributes to the standard fullname and email account attributes. These + maps are represented as key=value where the key equals the from_attribute, and + the value equals the to_attribute. For example, "preferredName=fullName". All + attribute names are case insensitive. + + ## Referenced By - [Account][] diff --git a/website/content/docs/concepts/domain-model/managed-groups.mdx b/website/content/docs/concepts/domain-model/managed-groups.mdx index 3d978ef438..d35e9175cb 100644 --- a/website/content/docs/concepts/domain-model/managed-groups.mdx +++ b/website/content/docs/concepts/domain-model/managed-groups.mdx @@ -41,6 +41,16 @@ OIDC managed groups have the following additional attributes: [filtering concepts]: /boundary/docs/concepts/filtering [oidc managed groups filtering]: /boundary/docs/concepts/filtering/oidc-managed-groups +### LDAP Managed Group Information and Attributes + +Membership in LDAP managed groups is evaluated when the auth method is used for +authentication, based on information contained within the LDAP server. Every +authentication results in a new evaluation of managed group membership. +LDAP managed groups have the following additional attributes: + +- `group-names` - (required) + A list of group names. + ## Referenced By - [Accounts][]