fix(audit): populate email/name in audit events
This commit is contained in:
1 parent
2c043d82fa
commit
fe35f74ee3
5 files changed
+404
-9
No files matched your search
@@ -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},
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user