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.
boundary/internal/auth/ldap/service_authenticate_test.go

362 lines
13 KiB

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package ldap
import (
"context"
"encoding/json"
"fmt"
"os"
"sync"
"testing"
"github.com/hashicorp/boundary/internal/authtoken"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/event"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/eventlogger/formatter_filters/cloudevents"
"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)
opt := event.TestWithObservationSink(t)
c := event.TestEventerConfig(t, "Test_StartAuth_to_Callback", opt)
testLock := &sync.Mutex{}
testLogger := hclog.New(&hclog.LoggerOptions{
Mutex: testLock,
Name: "test",
})
c.EventerConfig.TelemetryEnabled = true
require.NoError(t, event.InitSysEventer(testLogger, testLock, "use-Test_Authenticate", event.WithEventerConfig(&c.EventerConfig)))
// some standard factories for unit tests
authenticatorFn := func() (Authenticator, error) {
return NewRepository(testCtx, testRw, testRw, testKms)
}
lookupUserWithFn := func() (LookupUser, error) {
return iam.NewRepository(testCtx, testRw, testRw, testKms)
}
tokenCreatorFn := func() (AuthTokenCreator, error) {
return authtoken.NewRepository(testCtx, 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://%s:%d", td.Host(), 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://%s:%d", td.Host(), 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)
sinkFileName := c.ObservationEvents.Name()
defer func() { _ = os.WriteFile(sinkFileName, nil, 0o666) }()
b, err := os.ReadFile(sinkFileName)
require.NoError(err)
gotRes := &cloudevents.Event{}
err = json.Unmarshal(b, gotRes)
require.NoErrorf(err, "json: %s", string(b))
details, ok := gotRes.Data.(map[string]any)["details"]
require.True(ok)
for _, key := range details.([]any) {
assert.Contains(key.(map[string]any)["payload"], "user_id")
assert.Contains(key.(map[string]any)["payload"], "auth_token_start")
assert.Contains(key.(map[string]any)["payload"], "auth_token_end")
}
})
}
}
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
}