feature (auth/ldap): add LDAP auth method along with associated accounts and managed groups (#2912)

* feature (auth): required schema changes for auth ldap method (#2669)

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

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

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

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

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

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

* feature (auth/ldap): add Account

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

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

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

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

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

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

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

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

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

* chore (auth/ldap): make fmt deltas

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

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

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

* feature (auth/ldap): add managed groups

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* chore: make fmt changes

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

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

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

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

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

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

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

* chore (migrations): fix whitespace in stmt

* chore: fmt fixup

* tests (auth/ldap): invalid err msg

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* chore: add copyright headers

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

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

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

* tests (managed groups): add required errContains for new test
This commit is contained in:
Jim authored and GitHub committed 2023-03-15 11:40:40 -04:00
1 parent acda0c3892
commit 86192f75eb
121 files changed
+26652 -671

No files matched your search

+1
View File
@@ -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
@@ -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)
}
+24
View File
@@ -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"]
@@ -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)
}
+504
View File
@@ -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
}
}
@@ -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)
}
+24
View File
@@ -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
+8
View File
@@ -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
+8 -3
View File
@@ -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
+14 -2
View File
@@ -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=
+33
View File
@@ -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",
@@ -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
+146
View File
@@ -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
}
+151
View File
@@ -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
}
@@ -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)
}
})
}
+417
View File
@@ -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)
})
}
}
+319
View File
@@ -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
}
+594
View File
@@ -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))
}
+104
View File
@@ -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
}
+283
View File
@@ -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")
})
}
+92
View File
@@ -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
}
+152
View File
@@ -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))
})
}
+62
View File
@@ -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
}
@@ -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)
})
}
}
+120
View File
@@ -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
}
@@ -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")
})
}
@@ -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
}
@@ -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))
})
}
+52
View File
@@ -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
}
+106
View File
@@ -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
}
+222
View File
@@ -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)
})
}
}
+365
View File
@@ -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
}
}
+318
View File
@@ -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)
})
}
+55
View File
@@ -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
}
+254
View File
@@ -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
}
File diff suppressed because it is too large. Load diff
@@ -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
}
}
@@ -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...)
}
}
@@ -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
}
@@ -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)
})
}
}
@@ -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" }
@@ -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)
}
}
})
}
}
@@ -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
}
File diff suppressed because it is too large. Load diff
@@ -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
}
@@ -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)
}
@@ -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
}
@@ -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
}
@@ -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())
})
}
}
File diff suppressed because it is too large. Load diff
+121
View File
@@ -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)
})
}
}
+118
View File
@@ -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
}
+616
View File
@@ -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)
})
}
}
@@ -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)
}
@@ -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
}
+28
View File
@@ -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)
}
+27
View File
@@ -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)
}
})
}
}
File diff suppressed because it is too large. Load diff
+301
View File
@@ -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"
}
+121
View File
@@ -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())
}
+78
View File
@@ -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
}
+152
View File
@@ -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))
})
}
@@ -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
}
@@ -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))
})
}
+58
View File
@@ -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
}
+41
View File
@@ -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{
@@ -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 }
)
@@ -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
}
@@ -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
}
+197
View File
@@ -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)
}
@@ -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 }
)
@@ -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
}
@@ -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
}
@@ -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 }
)
+43
View File
@@ -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": {
{
+13 -2
View File
@@ -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
@@ -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)
+5
View File
@@ -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)
}
+2 -1
View File
@@ -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
}
+3 -3
View File
@@ -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)
}
@@ -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 {
File diff suppressed because it is too large. Load diff
@@ -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
@@ -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(),
@@ -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)
})
@@ -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{
@@ -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
}
@@ -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)
}
})
}
}
@@ -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{
@@ -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(),
@@ -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 {
File diff suppressed because it is too large. Load diff
@@ -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
@@ -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 {
@@ -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(),
+2 -1
View File
@@ -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
@@ -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)
+1 -1
View File
@@ -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)
}
@@ -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
@@ -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;
Loaded 100 of 121 files, more files were not shown because too many files have changed in this diff. Show more