mirror of https://github.com/hashicorp/boundary
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
473 lines
15 KiB
473 lines
15 KiB
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package ldap
|
|
|
|
import (
|
|
"context"
|
|
"database/sql/driver"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/DATA-DOG/go-sqlmock"
|
|
"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://%s:%d", td.Host(), td.Port())},
|
|
WithCertificates(testCtx, tdCerts...),
|
|
WithDiscoverDn(testCtx),
|
|
WithEnableGroups(testCtx),
|
|
WithUserDn(testCtx, testdirectory.DefaultUserDN),
|
|
WithGroupDn(testCtx, testdirectory.DefaultGroupDN),
|
|
)
|
|
|
|
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",
|
|
}),
|
|
)
|
|
|
|
const (
|
|
testLoginName = "alice"
|
|
testPassword = "password"
|
|
)
|
|
|
|
testAccount := TestAccount(t, testConn, testAm, testLoginName)
|
|
testAccountAccAttrs := TestAccount(t, testConn, testAmAccAttrs, 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"}),
|
|
gldap.NewEntryAttribute("fn", []string{"test-fn"}),
|
|
gldap.NewEntryAttribute("at", []string{"testat@email.com"}),
|
|
)
|
|
}
|
|
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-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
|
|
},
|
|
},
|
|
{
|
|
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",
|
|
},
|
|
{
|
|
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")`,
|
|
},
|
|
}
|
|
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://%s:%d", td.Host(), 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://%s:%d", td.Host(), 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)
|
|
})
|
|
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)
|
|
})
|
|
}
|
|
|
|
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)
|
|
}
|