From f9dee55e4d3f0dd07de083972124112cfd772a03 Mon Sep 17 00:00:00 2001 From: Sepehr <46660996+sepehrfrgh@users.noreply.github.com> Date: Mon, 30 Oct 2023 08:54:00 -0700 Subject: [PATCH] Telemetry(observations): add oidc and ldap observation events (#3945) * telemetry(observations): Add observations for oidc and ldap authentications * telemetry(observations): Add tests ldap and oidc observation events --- internal/auth/ldap/service_authenticate.go | 6 +- .../auth/ldap/service_authenticate_test.go | 29 ++++++- internal/auth/oidc/service_callback.go | 8 +- internal/auth/oidc/service_callback_test.go | 77 +++++++++++++++++-- .../handlers/authmethods/ldap_test.go | 30 +++++++- 5 files changed, 138 insertions(+), 12 deletions(-) diff --git a/internal/auth/ldap/service_authenticate.go b/internal/auth/ldap/service_authenticate.go index 864215d5d2..c0a28e02b1 100644 --- a/internal/auth/ldap/service_authenticate.go +++ b/internal/auth/ldap/service_authenticate.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/boundary/internal/authtoken" "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/event" "github.com/hashicorp/boundary/internal/iam" ) @@ -82,7 +83,10 @@ func Authenticate( if err != nil { return nil, errors.Wrap(ctx, err, op) } - + if err := event.WriteObservation(ctx, op, event.WithDetails("user_id", user.GetPublicId(), "auth_token_start", + token.GetCreateTime(), "auth_token_end", token.GetExpirationTime())); err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("Unable to write observation event for authenticate method")) + } return token, nil } diff --git a/internal/auth/ldap/service_authenticate_test.go b/internal/auth/ldap/service_authenticate_test.go index 27a22d4767..2a9e723c39 100644 --- a/internal/auth/ldap/service_authenticate_test.go +++ b/internal/auth/ldap/service_authenticate_test.go @@ -5,14 +5,20 @@ package ldap import ( "context" + "encoding/json" "fmt" + "io/ioutil" + "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" @@ -27,7 +33,14 @@ func TestAuthenticate(t *testing.T) { 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", + }) + 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) @@ -313,6 +326,20 @@ func TestAuthenticate(t *testing.T) { } require.NoError(err) assert.NotEmpty(got) + sinkFileName := c.ObservationEvents.Name() + defer func() { _ = os.WriteFile(sinkFileName, nil, 0o666) }() + b, err := ioutil.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") + } }) } } diff --git a/internal/auth/oidc/service_callback.go b/internal/auth/oidc/service_callback.go index b287c89419..8d90dd7188 100644 --- a/internal/auth/oidc/service_callback.go +++ b/internal/auth/oidc/service_callback.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/boundary/internal/auth/oidc/request" "github.com/hashicorp/boundary/internal/authtoken" "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/event" "github.com/hashicorp/cap/oidc" "github.com/hashicorp/go-bexpr" "github.com/mitchellh/pointerstructure" @@ -252,12 +253,17 @@ func Callback( if err != nil { return "", errors.Wrap(ctx, err, op) } - if _, err := tokenRepo.CreateAuthToken(ctx, user, acct.PublicId, authtoken.WithPublicId(reqState.TokenRequestId), authtoken.WithStatus(authtoken.PendingStatus)); err != nil { + authToken, err := tokenRepo.CreateAuthToken(ctx, user, acct.PublicId, authtoken.WithPublicId(reqState.TokenRequestId), authtoken.WithStatus(authtoken.PendingStatus)) + if err != nil { if errors.Match(errors.T(errors.NotUnique), err) { return "", errors.New(ctx, errors.Forbidden, op, "not a unique request", errors.WithWrap(err)) } return "", errors.Wrap(ctx, err, op) } + if err := event.WriteObservation(ctx, op, event.WithDetails("user_id", user.GetPublicId(), "auth_token_start", + authToken.GetCreateTime(), "auth_token_end", authToken.GetExpirationTime())); err != nil { + return "", errors.Wrap(ctx, err, op, errors.WithMsg("Unable to write observation event for authenticate method")) + } // tada! we can return a final redirect URL for the successful authentication. return reqState.FinalRedirectUrl, nil } diff --git a/internal/auth/oidc/service_callback_test.go b/internal/auth/oidc/service_callback_test.go index d61457252f..2af5d19b99 100644 --- a/internal/auth/oidc/service_callback_test.go +++ b/internal/auth/oidc/service_callback_test.go @@ -5,11 +5,14 @@ package oidc import ( "context" + "encoding/json" "fmt" "io/ioutil" "net/http" "net/http/httptest" "net/url" + "os" + "sync" "testing" "time" @@ -19,11 +22,14 @@ import ( "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" iamStore "github.com/hashicorp/boundary/internal/iam/store" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/oplog" "github.com/hashicorp/cap/oidc" + "github.com/hashicorp/eventlogger/formatter_filters/cloudevents" + "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-secure-stdlib/strutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -39,7 +45,14 @@ func Test_Callback(t *testing.T) { kmsCache := kms.TestKms(t, conn, rootWrapper) testCtx := context.Background() - + 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", + }) + require.NoError(t, event.InitSysEventer(testLogger, testLock, "use-Test_Callback", event.WithEventerConfig(&c.EventerConfig))) // some standard factories for unit tests which // are used in the Callback(...) call iamRepoFn := func() (*iam.Repository, error) { @@ -332,7 +345,6 @@ func Test_Callback(t *testing.T) { if len(info) > 0 { tp.SetUserInfoReply(info) } - gotRedirect, err := Callback(ctx, tt.oidcRepoFn, tt.iamRepoFn, @@ -367,6 +379,21 @@ func Test_Callback(t *testing.T) { require.NoError(err) assert.Equal(tt.wantFinalRedirect, gotRedirect) + sinkFileName := c.ObservationEvents.Name() + defer func() { _ = os.WriteFile(sinkFileName, nil, 0o666) }() + b, err := ioutil.ReadFile(sinkFileName) + require.NoError(err) + got := &cloudevents.Event{} + err = json.Unmarshal(b, got) + require.NoErrorf(err, "json: %s", string(b)) + details, ok := got.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") + } + // make sure a pending token was created. var tokens []*authtoken.AuthToken err = rw.SearchWhere(ctx, &tokens, "1=?", []any{1}) @@ -461,7 +488,18 @@ func Test_Callback(t *testing.T) { tp.SetUserInfoReply(map[string]any{"sub": wantSubject}) tp.SetExpectedAuthNonce(testNonce) - + config := event.EventerConfig{ + ObservationsEnabled: true, + } + testLock := &sync.Mutex{} + testLogger := hclog.New(&hclog.LoggerOptions{ + Mutex: testLock, + Name: "test", + }) + e, err := event.NewEventer(testLogger, testLock, "replay-attack-with-dup-state", config) + require.NoError(err) + ctx, err := event.NewEventerContext(ctx, e) + require.NoError(err) // the first request should succeed. gotRedirect, err := Callback(ctx, repoFn, @@ -496,7 +534,13 @@ func Test_StartAuth_to_Callback(t *testing.T) { t.Run("startAuth-to-Callback", func(t *testing.T) { assert, require := assert.New(t), require.New(t) ctx := context.Background() - + c := event.TestEventerConfig(t, "Test_StartAuth_to_Callback") + testLock := &sync.Mutex{} + testLogger := hclog.New(&hclog.LoggerOptions{ + Mutex: testLock, + Name: "test", + }) + require.NoError(event.InitSysEventer(testLogger, testLock, "use-Test_StartAuth_to_Callback", event.WithEventerConfig(&c.EventerConfig))) conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) // start with no tokens in the db @@ -615,7 +659,14 @@ func Test_ManagedGroupFiltering(t *testing.T) { rw := db.New(conn) rootWrapper := db.TestWrapper(t) kmsCache := kms.TestKms(t, conn, 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", + }) + require.NoError(t, event.InitSysEventer(testLogger, testLock, "use-Test_ManagedGroupFiltering", event.WithEventerConfig(&c.EventerConfig))) // some standard factories for unit tests which // are used in the Callback(...) call iamRepoFn := func() (*iam.Repository, error) { @@ -779,7 +830,6 @@ func Test_ManagedGroupFiltering(t *testing.T) { require.Equal(numUpdated, 1) require.NoError(err) } - // Run the callback _, err = Callback(ctx, repoFn, @@ -790,7 +840,20 @@ func Test_ManagedGroupFiltering(t *testing.T) { code, ) require.NoError(err) - + sinkFileName := c.ObservationEvents.Name() + defer func() { _ = os.WriteFile(sinkFileName, nil, 0o666) }() + b, err := ioutil.ReadFile(sinkFileName) + require.NoError(err) + got := &cloudevents.Event{} + err = json.Unmarshal(b, got) + require.NoErrorf(err, "json: %s", string(b)) + details, ok := got.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") + } // Ensure that we get the expected groups memberships, err := repo.ListManagedGroupMembershipsByMember(ctx, account.PublicId) require.NoError(err) diff --git a/internal/daemon/controller/handlers/authmethods/ldap_test.go b/internal/daemon/controller/handlers/authmethods/ldap_test.go index f3140afa01..e62c1f9711 100644 --- a/internal/daemon/controller/handlers/authmethods/ldap_test.go +++ b/internal/daemon/controller/handlers/authmethods/ldap_test.go @@ -5,8 +5,12 @@ package authmethods_test import ( "context" + "encoding/json" "fmt" + "io/ioutil" + "os" "strings" + "sync" "testing" "github.com/google/go-cmp/cmp" @@ -20,12 +24,14 @@ import ( "github.com/hashicorp/boundary/internal/daemon/controller/handlers/authmethods" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/event" 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/eventlogger/formatter_filters/cloudevents" "github.com/hashicorp/go-hclog" "github.com/jimlambrt/gldap" "github.com/jimlambrt/gldap/testdirectory" @@ -876,7 +882,14 @@ func TestAuthenticate_Ldap(t *testing.T) { testRootWrapper := db.TestWrapper(t) testKms := kms.TestKms(t, testConn, testRootWrapper) o, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testRootWrapper)) - + 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", + }) + require.NoError(t, event.InitSysEventer(testLogger, testLock, "use-Test_Authenticate", event.WithEventerConfig(&c.EventerConfig))) iamRepoFn := func() (*iam.Repository, error) { return iam.TestRepo(t, testConn, testRootWrapper), nil } @@ -1094,7 +1107,20 @@ func TestAuthenticate_Ldap(t *testing.T) { assert.Equal(aToken.GetCreatedTime(), aToken.GetApproximateLastUsedTime()) assert.Equal(testAm.GetPublicId(), aToken.GetAuthMethodId()) assert.Equal(tc.wantType, resp.GetType()) - + sinkFileName := c.ObservationEvents.Name() + defer func() { _ = os.WriteFile(sinkFileName, nil, 0o666) }() + b, err := ioutil.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") + } // support testing for pre-provisioned accounts if tc.acctId != "" { assert.Equal(tc.acctId, aToken.GetAccountId())