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.
362 lines
13 KiB
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
|
|
}
|