2023-03-15 11:40:40 -04:00
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package ldap
import (
"context"
2024-05-09 17:41:47 +01:00
"database/sql/driver"
2023-10-26 11:42:07 -04:00
"encoding/pem"
2023-03-15 11:40:40 -04:00
"fmt"
"testing"
2024-05-09 17:41:47 +01:00
"github.com/DATA-DOG/go-sqlmock"
2023-03-15 11:40:40 -04:00
"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 ,
2023-04-28 11:35:54 -04:00
[] string { fmt . Sprintf ( "ldaps://%s:%d" , td . Host (), td . Port ())},
2023-03-15 11:40:40 -04:00
WithCertificates ( testCtx , tdCerts ... ),
WithDiscoverDn ( testCtx ),
WithEnableGroups ( testCtx ),
WithUserDn ( testCtx , testdirectory . DefaultUserDN ),
WithGroupDn ( testCtx , testdirectory . DefaultGroupDN ),
)
2024-05-09 14:46:57 +01:00
testAmAccAttrs := TestAuthMethod ( t , testConn , orgDbWrapper , org . PublicId ,
[] string { fmt . Sprintf ( "ldaps://%s:%d" , td . Host (), td . Port ())},
WithCertificates ( testCtx , tdCerts ... ),
WithDiscoverDn ( testCtx ),
WithEnableGroups ( testCtx ),
WithUserDn ( testCtx , testdirectory . DefaultUserDN ),
WithGroupDn ( testCtx , testdirectory . DefaultGroupDN ),
WithAccountAttributeMap ( testCtx , map [ string ] AccountToAttribute {
"fn" : "fullName" ,
"at" : "email" ,
}),
)
2023-03-15 11:40:40 -04:00
const (
testLoginName = "alice"
testPassword = "password"
)
testAccount := TestAccount ( t , testConn , testAm , testLoginName )
2024-05-09 14:46:57 +01:00
testAccountAccAttrs := TestAccount ( t , testConn , testAmAccAttrs , testLoginName )
2023-03-15 11:40:40 -04:00
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" }),
2024-05-09 14:46:57 +01:00
gldap . NewEntryAttribute ( "fn" , [] string { "test-fn" }),
gldap . NewEntryAttribute ( "at" , [] string { "testat@email.com" }),
2023-03-15 11:40:40 -04:00
)
}
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
},
},
2024-05-09 14:46:57 +01:00
{
name : "success-with-account-acc-attrs" ,
ctx : testCtx ,
repo : testRepo ,
authMethodId : testAmAccAttrs . PublicId ,
loginName : testAccountAccAttrs . LoginName ,
password : testPassword ,
want : func ( got * Account ) * Account {
a := & Account { Account : & store . Account {
AuthMethodId : testAmAccAttrs . PublicId ,
ScopeId : testAccountAccAttrs . ScopeId ,
PublicId : testAccountAccAttrs . PublicId ,
Version : testAccountAccAttrs . Version ,
Dn : "cn=alice,ou=people,dc=example,dc=org" ,
Email : "testat@email.com" ,
FullName : "test-fn" ,
LoginName : "alice" ,
MemberOfGroups : "[\"cn=admin,ou=groups,dc=example,dc=org\"]" ,
}}
return a
},
},
2023-03-15 11:40:40 -04:00
{
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" ,
},
2024-05-09 17:41:47 +01:00
{
name : "invalid-attribute-maps" ,
ctx : testCtx ,
repo : func () * Repository {
conn , mock := db . TestSetupWithMock ( t )
mock . ExpectQuery ( "SELECT" ). WillReturnRows ( sqlmock . NewRows (
[] string {
"is_primary_auth_method" ,
"public_id" ,
"scope_id" ,
"name" ,
"description" ,
"create_time" ,
"update_time" ,
"version" ,
"state" ,
"start_tls" ,
"insecure_tls" ,
"discover_dn" ,
"anon_group_search" ,
"upn_domain" ,
"enable_groups" ,
"use_token_groups" ,
"maximum_page_size" ,
"urls" ,
"certs" ,
"account_attribute_map" ,
"user_dn" ,
"user_attr" ,
"user_filter" ,
"group_dn" ,
"group_attr" ,
"group_filter" ,
"client_certificate_key" ,
"client_certificate_key_hmac" ,
"client_certificate_key_id" ,
"client_certificate_cert" ,
"bind_dn" ,
"bind_password" ,
"bind_password_hmac" ,
"bind_password_key_id" ,
"dereference_aliases" ,
},
). AddRow (
"f" ,
"amldap_badattrs" ,
"o_1234567890" ,
nil ,
nil ,
"2024-05-15 17:40:31.327736+00" ,
"2024-05-15 17:40:31.327736+00" ,
1 ,
"inactive" ,
"f" ,
"f" ,
"t" ,
"f" ,
nil ,
"t" ,
"f" ,
0 ,
fmt . Sprintf ( "ldaps://%s:%d" , td . Host (), td . Port ()),
func () driver . Value {
certs , err := EncodeCertificates ( testCtx , tdCerts ... )
require . NoError ( t , err )
return certs [ 0 ]
}(),
"at=email|fn=fullName|bad=invalidAttribute" ,
"ou=people,dc=example,dc=org" ,
nil ,
nil ,
"ou=groups,dc=example,dc=org" ,
nil ,
nil ,
nil ,
nil ,
nil ,
nil ,
nil ,
nil ,
nil ,
nil ,
nil ,
))
rw := db . New ( conn )
r , err := NewRepository ( testCtx , rw , rw , testKms )
require . NoError ( t , err )
return r
}(),
authMethodId : "amldap_badattrs" ,
loginName : "alice" ,
password : testPassword ,
wantErrMatch : errors . T ( errors . InvalidParameter ),
wantErrContains : `failed to convert account attribute maps: ldap.(AuthMethod).convertAccountAttributeMaps: ldap.ParseAccountAttributeMaps: ldap.ConvertToAccountToAttribute: "invalidAttribute" is not a valid ToAccountAttribute value ("email", "fullName")` ,
},
2023-03-15 11:40:40 -04:00
}
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 ,
2023-04-28 11:35:54 -04:00
[] string { fmt . Sprintf ( "ldaps://%s:%d" , td . Host (), td . Port ())},
2023-03-15 11:40:40 -04:00
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 ,
2023-04-28 11:35:54 -04:00
[] string { fmt . Sprintf ( "ldaps://%s:%d" , td . Host (), td . Port ())},
2023-03-15 11:40:40 -04:00
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 )
})
2023-10-26 11:42:07 -04:00
t . Run ( "mTLS-client-success" , func ( t * testing . T ) {
// test for client key/cert issue reported:
// https://github.com/hashicorp/boundary/issues/3927
tdWithMtls := testdirectory . Start ( t ,
testdirectory . WithMTLS ( t ),
testdirectory . WithDefaults ( t , & testdirectory . Defaults { AllowAnonymousBind : true }),
testdirectory . WithLogger ( t , logger ),
)
tdWithMtlsCerts , err := ParseCertificates ( testCtx , tdWithMtls . Cert ())
require . NoError ( t , err )
testClientCert , err := ParseCertificates ( testCtx , tdWithMtls . ClientCert ())
require . NoError ( t , err )
testClientKeyPem := tdWithMtls . ClientKey ()
block , _ := pem . Decode ([] byte ( testClientKeyPem ))
require . NotEmpty ( t , block )
// block.Bytes
testAmWithMtls := TestAuthMethod ( t , testConn , orgDbWrapper , org . PublicId ,
[] string { fmt . Sprintf ( "ldaps://%s:%d" , tdWithMtls . Host (), tdWithMtls . Port ())},
WithCertificates ( testCtx , tdWithMtlsCerts ... ),
WithClientCertificate ( testCtx , block . Bytes , testClientCert [ 0 ]),
WithDiscoverDn ( testCtx ),
WithEnableGroups ( testCtx ),
WithUserDn ( testCtx , testdirectory . DefaultUserDN ),
WithGroupDn ( testCtx , testdirectory . DefaultGroupDN ),
)
testAccountUsingMtls := TestAccount ( t , testConn , testAmWithMtls , testLoginName )
tdWithMtls . SetUsers ( users ... )
tdWithMtls . SetGroups ( groups ... )
tdWithMtls . SetTokenGroups ( tokenGroups )
got , err := testRepo . Authenticate ( testCtx , testAmWithMtls . PublicId , testAccountUsingMtls . LoginName , testPassword )
require . NoError ( t , err )
require . NotEmpty ( t , got )
})
2023-03-15 11:40:40 -04:00
}
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 )
}