diff --git a/Makefile b/Makefile index c06399ba5..1ada4969a 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 000000000..d7628ff9a --- /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 0ab8ccc17..4a1d66bc9 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 000000000..ffe8900a6 --- /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 9ada6e4c7..4f793f7c5 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 000000000..3eeef101a --- /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 f91fc0fba..da6ab607d 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 ff70d8bcc..2efa9e485 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 268a4bc0d..133423baf 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 251c047bd..3ec4648b5 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 390f589c3..cb3f3c24c 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 8f5223b63..98c126da9 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 000000000..e692aaef9 --- /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 000000000..480e8f7d6 --- /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 000000000..00977f623 --- /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 000000000..9059a300c --- /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 000000000..95fca2442 --- /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 000000000..128e3dc78 --- /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 000000000..0787e567c --- /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 000000000..9cea7a07b --- /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 000000000..5b710006a --- /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 000000000..556956004 --- /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 000000000..b5c56f0cc --- /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 000000000..e71131c0d --- /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 000000000..ffef96b7d --- /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 000000000..1884cd5bd --- /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 000000000..822839288 --- /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 000000000..a8cec96e0 --- /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 000000000..2d5dd7764 --- /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 000000000..e93b85bb6 --- /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 000000000..1176f4eef --- /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 000000000..f4bc7b3f9 --- /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 000000000..21483ef79 --- /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 000000000..ef46a7b29 --- /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 000000000..e61fa122f --- /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 000000000..2b3908ca3 --- /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 000000000..0b6aed9b4 --- /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 000000000..05ce8327f --- /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 000000000..85e0787e0 --- /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 000000000..22c9c3025 --- /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 000000000..a4bccc38c --- /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 000000000..8f0da5018 --- /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 000000000..b3ba33702 --- /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 000000000..ed3d6d254 --- /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 000000000..f417adcf7 --- /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 000000000..7b76929c1 --- /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 000000000..b7f939856 --- /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 000000000..67f7d5e8f --- /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 000000000..69affea64 --- /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 000000000..6851e2426 --- /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 000000000..9a14ed4fb --- /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 000000000..a9dc39d07 --- /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 000000000..6dc80ddd8 --- /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 000000000..a1b5726e0 --- /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 000000000..5719a7261 --- /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 000000000..ccbf8d57b --- /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 000000000..1c65028a7 --- /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 000000000..d01792052 --- /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 000000000..a06213127 --- /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 000000000..837fb2622 --- /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 000000000..f94b9770e --- /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 000000000..afee45461 --- /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 000000000..703f654b1 --- /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 000000000..4ec77d65f --- /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 000000000..ee770b327 --- /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 b1daadc7d..af68cec9d 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 000000000..c5d8b8546 --- /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 000000000..82f19cf23 --- /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 aac1a7712..41335e564 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 000000000..d2071451a --- /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 000000000..852ccbeef --- /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 000000000..403cacc9a --- /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 000000000..500fa5557 --- /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 000000000..158288990 --- /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 0dbd655c3..8b34aaa64 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 98b136d71..1a058f76c 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 0b2c6e20e..66b62dcb6 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 6a6609595..39ee14a16 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 97b149394..8d9d95c7b 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 dbeb059d2..978045d7a 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 01ef20aed..81667ca8a 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") + 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") } - if oidcRepo == nil { - return Service{}, errors.NewDeprecated(errors.InvalidParameter, op, "missing oidc repository provided") - } - return Service{pwRepoFn: pwRepo, oidcRepoFn: oidcRepo}, nil + 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 7ac5ab20f..43efa922e 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,6 +1401,285 @@ func TestCreateOidc(t *testing.T) { } } +func TestCreateLdap(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) + } + + 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)) + 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.CreateAccountRequest + res *pbs.CreateAccountResponse + err error + errContains string + }{ + { + 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{ + 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, + }, + }, + }, + { + 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") @@ -1166,10 +1695,13 @@ func TestUpdatePassword(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] - tested, err := accounts.NewService(pwRepoFn, oidcRepoFn) + 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()} @@ -1553,6 +2085,9 @@ 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)) @@ -1565,7 +2100,7 @@ func TestUpdateOidc(t *testing.T) { oidc.WithSigningAlgs(oidc.RS256), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0])) - 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()} @@ -1925,6 +2460,380 @@ 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) + 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 := ldap.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, []string{"ldaps://ldap"}) + + 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_LdapAccountAttributes{ + LdapAccountAttributes: &pb.LdapAccountAttributes{ + LoginName: "test-login", + }, + } + + freshAccount := func(t *testing.T) (*ldap.Account, func()) { + t.Helper() + 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()), + &pbs.DeleteAccountRequest{Id: acc.GetPublicId()}) + require.NoError(t, err) + } + + return acc, clean + } + + cases := []struct { + name string + req *pbs.UpdateAccountRequest + res *pbs.UpdateAccountResponse + err error + errContains string + }{ + { + name: "update-existing", + 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: ldap.Subtype.String(), + }, + }, + res: &pbs.UpdateAccountResponse{ + Item: &pb.Account{ + 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.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name,description"}, + }, + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: ldap.Subtype.String(), + }, + }, + res: &pbs.UpdateAccountResponse{ + Item: &pb.Account{ + 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.UpdateAccountRequest{ + Item: &pb.Account{ + 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: "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), + errContains: "Cannot modify the resource type", + }, + { + 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"}, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "missing 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"}, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "No valid fields included in the update mask", + }, + { + name: "Unset Name", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{globals.NameField}, + }, + Item: &pb.Account{ + Description: &wrapperspb.StringValue{Value: "ignored"}, + }, + }, + res: &pbs.UpdateAccountResponse{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Description: &wrapperspb.StringValue{Value: "default"}, + Type: ldap.Subtype.String(), + Attrs: defaultAttributes, + Scope: defaultScopeInfo, + AuthorizedActions: ldapAuthorizedActions, + }, + }, + }, + { + 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"}, + }, + }, + res: &pbs.UpdateAccountResponse{ + Item: &pb.Account{ + 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.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{globals.DescriptionField}, + }, + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: "ignored"}, + Description: &wrapperspb.StringValue{Value: "notignored"}, + }, + }, + res: &pbs.UpdateAccountResponse{ + Item: &pb.Account{ + 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 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"}, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "Field cannot be updated.", + }, + { + name: "Update a Non Existing Account", + req: &pbs.UpdateAccountRequest{ + Id: globals.LdapAccountPrefix + "_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), + errContains: "Resource not found.", + }, + { + name: "Cant change Id", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"id"}, + }, + Item: &pb.Account{ + Id: globals.LdapAccountPrefix + "_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.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"created_time"}, + }, + Item: &pb.Account{ + 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.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"updated_time"}, + }, + Item: &pb.Account{ + 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.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"type"}, + }, + Item: &pb.Account{ + Type: "oidc", + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "name: \"type\", desc: \"Cannot modify the resource type.", + }, + { + name: "Update Login name", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.login_name"}, + }, + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: "ignored"}, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + errContains: "name: \"attributes.login_name\", desc: \"Field cannot be updated.", + }, + } + 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.GetPublicId() + } + + if tc.res != nil && tc.res.Item != nil { + 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) + require.NotEmpty(tc.errContains) + assert.Contains(gErr.Error(), tc.errContains) + return + } + + 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.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()), "UpdateAccount(%q) got response %q, wanted %q", tc.req, got, tc.res) + }) + } +} + func TestSetPassword(t *testing.T) { ctx := context.TODO() conn, _ := db.TestSetup(t, "postgres") @@ -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 56a292f7b..966b27304 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 cdd446155..6143d35c4 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 f9e5f570b..8a92e9358 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 1de49ff8e..b5db0f33f 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 000000000..59ef82092 --- /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 000000000..77441f0a5 --- /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 592b0db2d..8de3d0931 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 cf057d5e7..d8e975d4d 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 6a7dd649e..e47f20824 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 c9146932e..6906f3379 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: "Wrong id prefix", - req: &pbs.GetManagedGroupRequest{Id: "j_1234567890"}, - res: nil, - err: handlers.ApiErrorWithCode(codes.InvalidArgument), + 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), + 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), + errContains: "Invalid formatted identifier.", + }, + { + 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,7 +378,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 := 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) @@ -337,6 +412,164 @@ 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) + 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) + } + 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 TestDelete(t *testing.T) { ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") @@ -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,6 +914,212 @@ func TestCreateOidc(t *testing.T) { } } +func TestCreateLdap(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) + } + + 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 := ldap.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, []string{"ldaps://ldap1"}) + + cases := []struct { + name string + req *pbs.CreateManagedGroupRequest + res *pbs.CreateManagedGroupResponse + err error + errContains string + }{ + { + name: "Create a valid ManagedGroup", + req: &pbs.CreateManagedGroupRequest{ + Item: &pb.ManagedGroup{ + 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.CreateManagedGroupResponse{ + Uri: fmt.Sprintf("managed-groups/%s_", globals.LdapManagedGroupPrefix), + Item: &pb.ManagedGroup{ + 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: "Create a valid ManagedGroup without type defined", + req: &pbs.CreateManagedGroupRequest{ + Item: &pb.ManagedGroup{ + AuthMethodId: am.GetPublicId(), + Attrs: &pb.ManagedGroup_LdapManagedGroupAttributes{ + LdapManagedGroupAttributes: &pb.LdapManagedGroupAttributes{ + GroupNames: []string{"admin", "users"}, + }, + }, + }, + }, + res: &pbs.CreateManagedGroupResponse{ + Uri: fmt.Sprintf("managed-groups/%s_", globals.LdapManagedGroupPrefix), + Item: &pb.ManagedGroup{ + 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") @@ -662,6 +1132,9 @@ 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)) @@ -674,7 +1147,7 @@ func TestUpdateOidc(t *testing.T) { oidc.WithSigningAlgs(oidc.RS256), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0])) - tested, err := managed_groups.NewService(oidcRepoFn) + 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()} @@ -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 1758a4a8e..828082609 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 a7dae45b7..bc2cd53bd 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 a79423f3e..c4664cecf 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 a221c188a..4182dc78b 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 adff98b60..250fb8170 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 084303d59..6dcc76117 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 44188e0ba..fad1232c8 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 000000000..294f3d67d --- /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 000000000..63524c9ad --- /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 d786af683..ac417c377 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 9b3e2ae5a..e7fa270b9 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 000000000..aaa74bc7b --- /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 000000000..49c8d97a1 --- /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 fe96d672e..5669e1c20 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 95226a175..e06b30cfb 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, + 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, 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, - 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 b454cdabc..f46b2d02a 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 5345e5065..c71b66305 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 f67bca0d2..439e2c891 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 9aa9613f8..8ad4da793 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 000000000..034c57b83 --- /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 53ea92410..bb154f0d3 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 a487826f4..2fd3d2648 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 30d67f71f..d80c97e20 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 ce442a075..800823a30 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 7a1c9d8fe..65d66bc58 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, - 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, + 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, 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, + 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, 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, + 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, 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, 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, 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, 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 0e2a5e86f..73975ee26 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 4570b297b..f47126468 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 f92f30ada..191f40bc0 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 3d978ef43..d35e9175c 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][]