diff --git a/internal/cmd/base/dev.go b/internal/cmd/base/dev.go index c5ffea9b1b..55341ba474 100644 --- a/internal/cmd/base/dev.go +++ b/internal/cmd/base/dev.go @@ -289,7 +289,7 @@ func (b *Server) CreateDevLdapAuthMethod(ctx context.Context) error { createUserFn := func(userName, passwd string, withMembersOf []string) *gldap.Entry { entryAttrs := map[string][]string{ - "name": {userName}, + "fullName": {userName}, "email": {fmt.Sprintf("%s@localhost", userName)}, "password": {passwd}, } diff --git a/internal/daemon/controller/auth/auth.go b/internal/daemon/controller/auth/auth.go index 759d7608ec..9d0bac35c4 100644 --- a/internal/daemon/controller/auth/auth.go +++ b/internal/daemon/controller/auth/auth.go @@ -287,7 +287,6 @@ func Verify(ctx context.Context, resourceType resource.Type, opt ...Option) (ret resourcesToFetchGrants := append([]resource.Type{resourceType}, opts.withFetchAdditionalResourceGrants...) var authResults perms.ACLResults - var userData template.Data var err error authResults, ret.UserData, ret.Scope, v.acl, ret.grants, err = v.performAuthCheck(ctx, resourcesToFetchGrants, opts.withRecursive) if err != nil { @@ -345,17 +344,17 @@ func Verify(ctx context.Context, resourceType resource.Type, opt ...Option) (ret ea.UserInfo = &event.UserInfo{ UserId: ret.UserId, } - if userData.Account.Id != nil { - ea.UserInfo.AuthAccountId = *userData.Account.Id + if ret.UserData.Account.Id != nil { + ea.UserInfo.AuthAccountId = *ret.UserData.Account.Id } ea.GrantsInfo = &event.GrantsInfo{ Grants: grants, } - if userData.User.FullName != nil { - ea.UserName = *userData.User.FullName + if ret.UserData.User.FullName != nil { + ea.UserName = *ret.UserData.User.FullName } - if userData.User.Email != nil { - ea.UserEmail = *userData.User.Email + if ret.UserData.User.Email != nil { + ea.UserEmail = *ret.UserData.User.Email } if reqInfo != nil { diff --git a/internal/daemon/controller/auth/auth_test.go b/internal/daemon/controller/auth/auth_test.go index e4e972cd5e..f6f852fd10 100644 --- a/internal/daemon/controller/auth/auth_test.go +++ b/internal/daemon/controller/auth/auth_test.go @@ -14,6 +14,8 @@ import ( "testing" "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/auth/ldap" + "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/authtoken" "github.com/hashicorp/boundary/internal/daemon/controller/handlers" "github.com/hashicorp/boundary/internal/db" @@ -146,7 +148,7 @@ func TestAuthTokenAuthenticator(t *testing.T) { } } -func TestVerify_AuditEvent(t *testing.T) { +func TestVerify_RedactedAuth_AuditEvent(t *testing.T) { ctx := context.Background() eventConfig := event.TestEventerConfig(t, "Test_Verify", event.TestWithAuditSink(t)) testLock := &sync.Mutex{} @@ -266,6 +268,335 @@ func TestVerify_AuditEvent(t *testing.T) { } } +func TestVerify_UnredactedLdapAuth_AuditEvent(t *testing.T) { + ctx := context.Background() + eventConfig := event.TestEventerConfig(t, "Test_Verify", event.TestWithAuditSink(t)) + + // Disable redaction so we can assert on actual values + for _, sink := range eventConfig.EventerConfig.Sinks { + if sink.AuditConfig != nil { + sink.AuditConfig.FilterOverrides = event.AuditFilterOperations{ + event.SensitiveClassification: event.NoOperation, + } + } + } + + testLock := &sync.Mutex{} + testLogger := hclog.New(&hclog.LoggerOptions{ + Mutex: testLock, + Name: "test", + }) + require.NoError(t, event.InitSysEventer(testLogger, testLock, "Test_Verify", event.WithEventerConfig(&eventConfig.EventerConfig))) + + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, conn, wrapper) + tokenRepo, err := authtoken.NewRepository(ctx, rw, rw, testKms) + require.NoError(t, err) + iamRepo := iam.TestRepo(t, conn, wrapper) + tokenRepoFn := func() (*authtoken.Repository, error) { + return tokenRepo, nil + } + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + serversRepoFn := func() (*server.Repository, error) { + return server.NewRepository(ctx, rw, rw, testKms) + } + + // ok lets create a LDAP auth token + o, _ := iam.TestScopes(t, iamRepo) + databaseWrapper, err := testKms.GetWrapper(ctx, o.GetPublicId(), kms.KeyPurposeDatabase) + require.NoError(t, err) + testAuthMethod := ldap.TestAuthMethod(t, conn, databaseWrapper, o.GetPublicId(), []string{"ldaps://ldap1"}) + ldapAcct := ldap.TestAccount(t, conn, testAuthMethod, "freyja", + ldap.WithFullName(ctx, "Freyja Happy Doggo"), + ldap.WithEmail(ctx, "you@hellothere.com"), + ) + foundLdapAcct := ldap.AllocAccount() + foundLdapAcct.PublicId = ldapAcct.GetPublicId() + require.NoError(t, rw.LookupById(ctx, foundLdapAcct)) + require.Equal(t, "Freyja Happy Doggo", foundLdapAcct.FullName) + require.Equal(t, "you@hellothere.com", foundLdapAcct.Email) + + user := iam.TestUser(t, iamRepo, o.GetPublicId()) + _, err = iamRepo.AddUserAccounts(ctx, user.PublicId, user.Version, []string{ldapAcct.GetPublicId()}) + require.NoError(t, err) + + // set ldap auth method as primary + s, err := iamRepo.LookupScope(ctx, o.PublicId) + require.NoError(t, err) + iam.TestSetPrimaryAuthMethod(t, iamRepo, s, testAuthMethod.GetPublicId()) + + // ok lets verify we have a fullName and email to populated in the user + lookedUpUser, _, err := iamRepo.LookupUser(ctx, user.PublicId) + require.NoError(t, err) + require.Equal(t, "Freyja Happy Doggo", lookedUpUser.FullName) + require.Equal(t, "you@hellothere.com", lookedUpUser.Email) + + at, err := tokenRepo.CreateAuthToken(ctx, user, ldapAcct.GetPublicId()) + require.NoError(t, err) + encToken, err := authtoken.EncryptToken(context.Background(), testKms, o.GetPublicId(), at.GetPublicId(), at.GetToken()) + require.NoError(t, err) + + tokValue := at.GetPublicId() + "_" + encToken + jsCookieVal, httpCookieVal := tokValue[:len(tokValue)/2], tokValue[len(tokValue)/2:] + + tests := []struct { + name string + headers map[string]string + cookies []http.Cookie + opt []Option + disableAuth bool + disableAuthzFails bool + wantResults VerifyResults + wantUserId string + wantNameEmail bool + }{ + { + name: "bearer-token", + headers: map[string]string{"Authorization": fmt.Sprintf("Bearer %s", tokValue)}, + opt: []Option{WithScopeId(o.PublicId)}, + wantUserId: at.IamUserId, + wantNameEmail: true, + }, + { + name: "split-cookie-token", + cookies: []http.Cookie{ + {Name: handlers.HttpOnlyCookieName, Value: httpCookieVal}, + {Name: handlers.JsVisibleCookieName, Value: jsCookieVal}, + }, + opt: []Option{WithScopeId(o.PublicId)}, + wantUserId: at.IamUserId, + wantNameEmail: true, + }, + { + name: "no-auth-data", + opt: []Option{WithScopeId(o.PublicId)}, + wantUserId: globals.AnonymousUserId, + }, + { + name: "disable-auth", + opt: []Option{WithScopeId(o.PublicId)}, + disableAuth: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + req := httptest.NewRequest("GET", "http://127.0.0.1/v1/scopes/o_1", nil) + for k, v := range tt.headers { + req.Header.Set(k, v) + } + for _, c := range tt.cookies { + req.AddCookie(&c) + } + + // Add values for authn/authz checking + requestInfo := authpb.RequestInfo{ + Path: req.URL.Path, + Method: req.Method, + DisableAuthEntirely: tt.disableAuth, + DisableAuthzFailures: true, // we skipped giving grants so disable authz + } + requestInfo.PublicId, requestInfo.EncryptedToken, requestInfo.TokenFormat = GetTokenFromRequest(context.TODO(), testKms, req) + + ctx := NewVerifierContext(ctx, iamRepoFn, tokenRepoFn, serversRepoFn, testKms, &requestInfo) + + _ = os.WriteFile(eventConfig.AuditEvents.Name(), nil, 0o666) // clean out audit events from previous calls + _ = Verify(ctx, resource.Scope, tt.opt...) + got := api.CloudEventFromFile(t, eventConfig.AuditEvents.Name()) + + auth, ok := got.Data.(map[string]any)["auth"].(map[string]any) + require.True(ok) + + if tt.wantNameEmail { + assert.Equal("Freyja Happy Doggo", auth["name"]) + assert.Equal("you@hellothere.com", auth["email"]) + } else { + assert.Nil(auth["name"]) + assert.Nil(auth["email"]) + } + + if tt.disableAuth { + assert.Equal(true, auth["disabled_auth_entirely"]) + } + if tt.wantUserId != "" { + userInfo, ok := auth["user_info"].(map[string]any) + require.True(ok) + assert.Equal(tt.wantUserId, userInfo["id"]) + } + }) + } +} + +func TestVerify_UnredactedOidcAuth_AuditEvent(t *testing.T) { + ctx := context.Background() + eventConfig := event.TestEventerConfig(t, "Test_Verify", event.TestWithAuditSink(t)) + + // Disable redaction so we can assert on actual values + for _, sink := range eventConfig.EventerConfig.Sinks { + if sink.AuditConfig != nil { + sink.AuditConfig.FilterOverrides = event.AuditFilterOperations{ + event.SensitiveClassification: event.NoOperation, + } + } + } + + testLock := &sync.Mutex{} + testLogger := hclog.New(&hclog.LoggerOptions{ + Mutex: testLock, + Name: "test", + }) + require.NoError(t, event.InitSysEventer(testLogger, testLock, "Test_Verify", event.WithEventerConfig(&eventConfig.EventerConfig))) + + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, conn, wrapper) + tokenRepo, err := authtoken.NewRepository(ctx, rw, rw, testKms) + require.NoError(t, err) + iamRepo := iam.TestRepo(t, conn, wrapper) + tokenRepoFn := func() (*authtoken.Repository, error) { + return tokenRepo, nil + } + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + serversRepoFn := func() (*server.Repository, error) { + return server.NewRepository(ctx, rw, rw, testKms) + } + + // ok lets create a OIDC auth token + o, _ := iam.TestScopes(t, iamRepo) + databaseWrapper, err := testKms.GetWrapper(ctx, o.GetPublicId(), kms.KeyPurposeDatabase) + require.NoError(t, err) + testAuthMethod := oidc.TestAuthMethod(t, conn, databaseWrapper, o.GetPublicId(), oidc.ActivePrivateState, + "alice-rp", "fido", + oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://hellothere.com")[0]), + oidc.WithSigningAlgs(oidc.RS256), + oidc.WithApiUrl(oidc.TestConvertToUrls(t, "http://localhost")[0]), + ) + oidcAcct := oidc.TestAccount(t, conn, testAuthMethod, "Freyja", + oidc.WithFullName("Freyja Good Doggo"), + oidc.WithEmail("me@hellothere.com"), + ) + + user := iam.TestUser(t, iamRepo, o.GetPublicId()) + _, err = iamRepo.AddUserAccounts(ctx, user.PublicId, user.Version, []string{oidcAcct.GetPublicId()}) + require.NoError(t, err) + + s, err := iamRepo.LookupScope(ctx, o.PublicId) + require.NoError(t, err) + + // set oidcg auth method as primary + iam.TestSetPrimaryAuthMethod(t, iamRepo, s, testAuthMethod.GetPublicId()) + + // ok lets verify we have a fullName and email to populated in the user + lookedUpUser, _, err := iamRepo.LookupUser(ctx, user.PublicId) + require.NoError(t, err) + require.Equal(t, "Freyja Good Doggo", lookedUpUser.FullName) + require.Equal(t, "me@hellothere.com", lookedUpUser.Email) + + at, err := tokenRepo.CreateAuthToken(ctx, user, oidcAcct.GetPublicId()) + require.NoError(t, err) + encToken, err := authtoken.EncryptToken(context.Background(), testKms, o.GetPublicId(), at.GetPublicId(), at.GetToken()) + require.NoError(t, err) + + tokValue := at.GetPublicId() + "_" + encToken + jsCookieVal, httpCookieVal := tokValue[:len(tokValue)/2], tokValue[len(tokValue)/2:] + + tests := []struct { + name string + headers map[string]string + cookies []http.Cookie + opt []Option + disableAuth bool + disableAuthzFails bool + wantResults VerifyResults + wantUserId string + wantNameEmail bool + }{ + { + name: "bearer-token", + headers: map[string]string{"Authorization": fmt.Sprintf("Bearer %s", tokValue)}, + opt: []Option{WithScopeId(o.PublicId)}, + wantUserId: at.IamUserId, + wantNameEmail: true, + }, + { + name: "split-cookie-token", + cookies: []http.Cookie{ + {Name: handlers.HttpOnlyCookieName, Value: httpCookieVal}, + {Name: handlers.JsVisibleCookieName, Value: jsCookieVal}, + }, + opt: []Option{WithScopeId(o.PublicId)}, + wantUserId: at.IamUserId, + wantNameEmail: true, + }, + { + name: "no-auth-data", + opt: []Option{WithScopeId(o.PublicId)}, + wantUserId: globals.AnonymousUserId, + }, + { + name: "disable-auth", + opt: []Option{WithScopeId(o.PublicId)}, + disableAuth: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + req := httptest.NewRequest("GET", "http://127.0.0.1/v1/scopes/o_1", nil) + for k, v := range tt.headers { + req.Header.Set(k, v) + } + for _, c := range tt.cookies { + req.AddCookie(&c) + } + + // Add values for authn/authz checking + requestInfo := authpb.RequestInfo{ + Path: req.URL.Path, + Method: req.Method, + DisableAuthEntirely: tt.disableAuth, + DisableAuthzFailures: true, // we skipped giving grants so disable authz + } + requestInfo.PublicId, requestInfo.EncryptedToken, requestInfo.TokenFormat = GetTokenFromRequest(context.TODO(), testKms, req) + + ctx := NewVerifierContext(ctx, iamRepoFn, tokenRepoFn, serversRepoFn, testKms, &requestInfo) + + _ = os.WriteFile(eventConfig.AuditEvents.Name(), nil, 0o666) // clean out audit events from previous calls + _ = Verify(ctx, resource.Scope, tt.opt...) + got := api.CloudEventFromFile(t, eventConfig.AuditEvents.Name()) + + auth, ok := got.Data.(map[string]any)["auth"].(map[string]any) + require.True(ok) + + if tt.wantNameEmail { + assert.Equal("Freyja Good Doggo", auth["name"]) + assert.Equal("me@hellothere.com", auth["email"]) + } else { + assert.Nil(auth["name"]) + assert.Nil(auth["email"]) + } + + if tt.disableAuth { + assert.Equal(true, auth["disabled_auth_entirely"]) + } + if tt.wantUserId != "" { + userInfo, ok := auth["user_info"].(map[string]any) + require.True(ok) + assert.Equal(tt.wantUserId, userInfo["id"]) + } + }) + } +} + func TestGrantsHash(t *testing.T) { ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") diff --git a/internal/db/schema/migrations/oss/postgres/104/01_iam_acct_info_ldap.up.sql b/internal/db/schema/migrations/oss/postgres/104/01_iam_acct_info_ldap.up.sql new file mode 100644 index 0000000000..a5b95b4a31 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/104/01_iam_acct_info_ldap.up.sql @@ -0,0 +1,64 @@ +-- Copyright IBM Corp. 2026 +-- SPDX-License-Identifier: BUSL-1.1 + +-- Replaces iam_acct_info and iam_user_acct_info from 4/01_iam.up.sql +-- Add auth_ldap_account to iam_acct_info so that LDAP users with a primary +-- LDAP auth method have their full_name and email surfaced through +-- iam_user_acct_info. + +begin; + + drop view iam_user_acct_info; + drop view iam_acct_info; + + create view iam_acct_info as + select aa.iam_user_id, + oa.subject as login_name, + oa.public_id as primary_account_id, + oa.full_name as full_name, + oa.email as email + from iam_scope s, + auth_account aa, + auth_oidc_account oa + where aa.public_id = oa.public_id + and aa.auth_method_id = s.primary_auth_method_id + union + select aa.iam_user_id, + pa.login_name as login_name, + pa.public_id as primary_account_id, + '' as full_name, + '' as email + from iam_scope s, + auth_account aa, + auth_password_account pa + where aa.public_id = pa.public_id + and aa.auth_method_id = s.primary_auth_method_id + union + select aa.iam_user_id, + la.login_name as login_name, + la.public_id as primary_account_id, + la.full_name as full_name, + la.email as email + from iam_scope s, + auth_account aa, + auth_ldap_account la + where aa.public_id = la.public_id + and aa.auth_method_id = s.primary_auth_method_id; + + create view iam_user_acct_info as + select u.public_id, + u.scope_id, + u.name, + u.description, + u.create_time, + u.update_time, + u.version, + i.primary_account_id, + i.login_name, + i.full_name, + i.email + from iam_user u + left outer join iam_acct_info i + on u.public_id = i.iam_user_id; + +commit; diff --git a/internal/db/schema/migrations/oss/postgres/4/01_iam.up.sql b/internal/db/schema/migrations/oss/postgres/4/01_iam.up.sql index 3389237251..12a36b5d8f 100644 --- a/internal/db/schema/migrations/oss/postgres/4/01_iam.up.sql +++ b/internal/db/schema/migrations/oss/postgres/4/01_iam.up.sql @@ -5,6 +5,7 @@ begin; -- fix ordering of fields in iam_acct_info for auth_password_account select -- portion of union. requires recreating both views because of deps. +-- replaced by 104/01_iam_acct_info_ldap.up.sql drop view iam_user_acct_info; drop view iam_acct_info;