Skip to content

Commit

Permalink
audit logs for auth tokens and secrets
Browse files Browse the repository at this point in the history
  • Loading branch information
irshadaj committed Jan 31, 2024
1 parent 6810bd3 commit e8c6158
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 54 deletions.
21 changes: 11 additions & 10 deletions cmd/api/src/api/v2/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package auth

import (
"context"
"fmt"
"io"
"net/http"
Expand Down Expand Up @@ -608,19 +609,19 @@ func (s ManagementResource) DeleteUser(response http.ResponseWriter, request *ht
}
}

func (s ManagementResource) setUserSecret(user model.User, authSecret model.AuthSecret) error {
func (s ManagementResource) setUserSecret(ctx context.Context, user model.User, authSecret model.AuthSecret) error {
if user.AuthSecret != nil {
user.AuthSecret.Digest = authSecret.Digest
user.AuthSecret.DigestMethod = authSecret.DigestMethod
user.AuthSecret.ExpiresAt = authSecret.ExpiresAt.UTC()

if err := s.db.UpdateAuthSecret(*user.AuthSecret); err != nil {
if err := s.db.UpdateAuthSecret(ctx, *user.AuthSecret); err != nil {
return api.FormatDatabaseError(err)
} else {
return nil
}
} else {
if _, err := s.db.CreateAuthSecret(authSecret); err != nil {
if _, err := s.db.CreateAuthSecret(ctx, authSecret); err != nil {
return api.FormatDatabaseError(err)
} else {
return nil
Expand Down Expand Up @@ -663,7 +664,7 @@ func (s ManagementResource) PutUserAuthSecret(response http.ResponseWriter, requ
authSecret.ExpiresAt = time.Time{}
}

if err := s.setUserSecret(targetUser, authSecret); err != nil {
if err := s.setUserSecret(request.Context(), targetUser, authSecret); err != nil {
api.HandleDatabaseError(request, response, err)
} else {
response.WriteHeader(http.StatusOK)
Expand All @@ -686,7 +687,7 @@ func (s ManagementResource) ExpireUserAuthSecret(response http.ResponseWriter, r
authSecret := targetUser.AuthSecret
authSecret.ExpiresAt = time.Time{}

if err := s.db.UpdateAuthSecret(*authSecret); err != nil {
if err := s.db.UpdateAuthSecret(request.Context(), *authSecret); err != nil {
api.HandleDatabaseError(request, response, err)
} else {
// NOTE: This "should" be a 204 since we're not returning a payload but am returning a 200 to retain
Expand Down Expand Up @@ -789,7 +790,7 @@ func (s ManagementResource) CreateAuthToken(response http.ResponseWriter, reques
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusForbidden, err.Error(), request), response)
} else if authToken, err := auth.NewUserAuthToken(createUserTokenRequest.UserID, createUserTokenRequest.TokenName, auth.HMAC_SHA2_256); err != nil {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, api.ErrorResponseDetailsInternalServerError, request), response)
} else if newAuthToken, err := s.db.CreateAuthToken(authToken); err != nil {
} else if newAuthToken, err := s.db.CreateAuthToken(request.Context(), authToken); err != nil {
api.HandleDatabaseError(request, response, err)
} else {
api.WriteBasicResponse(request.Context(), newAuthToken, http.StatusOK, response)
Expand Down Expand Up @@ -827,7 +828,7 @@ func (s ManagementResource) DeleteAuthToken(response http.ResponseWriter, reques
} else if token.UserID.Valid && token.UserID.UUID != user.ID && !s.authorizer.AllowsPermission(bhCtx.AuthCtx, auth.Permissions().AuthManageUsers) {
log.Errorf("Bad user ID: %s != %s", token.UserID.UUID.String(), user.ID.String())
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusNotFound, api.ErrorResponseDetailsResourceNotFound, request), response)
} else if err := s.db.DeleteAuthToken(token); err != nil {
} else if err := s.db.DeleteAuthToken(request.Context(), token); err != nil {
api.HandleDatabaseError(request, response, err)
} else {
response.WriteHeader(http.StatusOK)
Expand Down Expand Up @@ -886,7 +887,7 @@ func (s ManagementResource) EnrollMFA(response http.ResponseWriter, request *htt
} else {
user.AuthSecret.TOTPSecret = totpSecret.Secret()

if err := s.db.UpdateAuthSecret(*user.AuthSecret); err != nil {
if err := s.db.UpdateAuthSecret(request.Context(), *user.AuthSecret); err != nil {
api.HandleDatabaseError(request, response, err)
} else if qrCode, err := auth.GenerateQRCodeBase64(*totpSecret); err != nil {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, api.ErrorResponseDetailsInternalServerError, request), response)
Expand Down Expand Up @@ -941,7 +942,7 @@ func (s ManagementResource) DisenrollMFA(response http.ResponseWriter, request *
user.AuthSecret.TOTPSecret = ""
user.AuthSecret.TOTPActivated = false

if err := s.db.UpdateAuthSecret(*user.AuthSecret); err != nil {
if err := s.db.UpdateAuthSecret(request.Context(), *user.AuthSecret); err != nil {
api.HandleDatabaseError(request, response, err)
} else {
responseBody := MFAStatusResponse{MFADeactivated}
Expand Down Expand Up @@ -992,7 +993,7 @@ func (s ManagementResource) ActivateMFA(response http.ResponseWriter, request *h
} else {
user.AuthSecret.TOTPActivated = true

if err := s.db.UpdateAuthSecret(*user.AuthSecret); err != nil {
if err := s.db.UpdateAuthSecret(request.Context(), *user.AuthSecret); err != nil {
api.HandleDatabaseError(request, response, err)
} else {
responseBody := MFAStatusResponse{MFAActivated}
Expand Down
10 changes: 5 additions & 5 deletions cmd/api/src/api/v2/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func TestManagementResource_PutUserAuthSecret(t *testing.T) {
Duration: appcfg.DefaultPasswordExpirationWindow,
}),
}, nil).Times(1)
mockDB.EXPECT().CreateAuthSecret(gomock.Any()).Return(model.AuthSecret{}, nil).Times(1)
mockDB.EXPECT().CreateAuthSecret(gomock.Any(), gomock.Any()).Return(model.AuthSecret{}, nil).Times(1)

// Happy path
test.Request(t).
Expand Down Expand Up @@ -758,7 +758,7 @@ func TestExpireUserAuthSecret_Success(t *testing.T) {
resources, mockDB := apitest.NewAuthManagementResource(mockCtrl)

mockDB.EXPECT().GetUser(userId).Return(model.User{AuthSecret: &model.AuthSecret{}}, nil)
mockDB.EXPECT().UpdateAuthSecret(gomock.Any()).Return(nil)
mockDB.EXPECT().UpdateAuthSecret(gomock.Any(), gomock.Any()).Return(nil)

ctx := context.WithValue(context.Background(), ctx.ValueKey, &ctx.Context{})
if req, err := http.NewRequestWithContext(ctx, "DELETE", fmt.Sprintf(endpoint, userId), nil); err != nil {
Expand Down Expand Up @@ -2533,7 +2533,7 @@ func TestDisenrollMFA_Success(t *testing.T) {
userId := test.NewUUIDv4(t)

mockDB.EXPECT().GetUser(userId).Return(model.User{AuthSecret: defaultDigestAuthSecret(t, "password")}, nil)
mockDB.EXPECT().UpdateAuthSecret(gomock.Any()).Return(nil)
mockDB.EXPECT().UpdateAuthSecret(gomock.Any(), gomock.Any()).Return(nil)

input := auth.MFAEnrollmentRequest{"password"}

Expand Down Expand Up @@ -2573,7 +2573,7 @@ func TestDisenrollMFA_Admin_Success(t *testing.T) {
nonAdminId := test.NewUUIDv4(t)

mockDB.EXPECT().GetUser(nonAdminId).Return(model.User{AuthSecret: defaultDigestAuthSecret(t, "password")}, nil)
mockDB.EXPECT().UpdateAuthSecret(gomock.Any()).Return(nil)
mockDB.EXPECT().UpdateAuthSecret(gomock.Any(), gomock.Any()).Return(nil)

adminContext := context.WithValue(context.Background(), ctx.ValueKey, &ctx.Context{})
bhCtx := ctx.Get(adminContext)
Expand Down Expand Up @@ -2854,7 +2854,7 @@ func TestActivateMFA_Success(t *testing.T) {
endpoint := "/api/v2/auth/users/%s/mfa-activation"
userId := test.NewUUIDv4(t)
mockDB.EXPECT().GetUser(userId).Return(model.User{AuthSecret: defaultDigestAuthSecretWithTOTP(t, "password", totpSecret.Secret())}, nil)
mockDB.EXPECT().UpdateAuthSecret(gomock.Any()).Return(nil)
mockDB.EXPECT().UpdateAuthSecret(gomock.Any(), gomock.Any()).Return(nil)

ctx := context.WithValue(context.Background(), ctx.ValueKey, &ctx.Context{})
inputBody := auth.MFAActivationRequest{passcode}
Expand Down
50 changes: 35 additions & 15 deletions cmd/api/src/database/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -442,13 +442,15 @@ func (s *BloodhoundDB) LookupUser(name string) (model.User, error) {

// CreateAuthToken creates a new AuthToken row using the provided struct
// INSERT INTO auth_tokens (...) VALUES (....)
func (s *BloodhoundDB) CreateAuthToken(authToken model.AuthToken) (model.AuthToken, error) {
var (
updatedAuthToken = authToken
result = s.db.Create(&updatedAuthToken)
)
func (s *BloodhoundDB) CreateAuthToken(ctx context.Context, authToken model.AuthToken) (model.AuthToken, error) {
auditEntry := model.AuditEntry{
Action: "CreateAuthToken",
Model: &authToken,
}

return updatedAuthToken, CheckError(result)
return authToken, s.AuditableTransaction(ctx, auditEntry, func(tx *gorm.DB) error {
return CheckError(tx.Create(&authToken))
})
}

// UpdateAuthToken updates all fields in the AuthToken row as specified in the provided struct
Expand Down Expand Up @@ -518,16 +520,28 @@ func (s *BloodhoundDB) GetUserToken(userId, tokenId uuid.UUID) (model.AuthToken,

// DeleteAuthToken deletes the provided AuthToken row
// DELETE FROM auth_tokens WHERE id = ...
func (s *BloodhoundDB) DeleteAuthToken(authToken model.AuthToken) error {
result := s.db.Where("id = ?", authToken.ID).Delete(&authToken)
return CheckError(result)
func (s *BloodhoundDB) DeleteAuthToken(ctx context.Context, authToken model.AuthToken) error {
auditEntry := model.AuditEntry{
Action: "DeleteAuthToken",
Model: &authToken,
}

return s.AuditableTransaction(ctx, auditEntry, func(tx *gorm.DB) error {
return CheckError(tx.Where("id = ?", authToken.ID).Delete(&authToken))
})
}

// CreateAuthSecret creates a new AuthSecret row
// INSERT INTO auth_secrets (...) VALUES (....)
func (s *BloodhoundDB) CreateAuthSecret(authSecret model.AuthSecret) (model.AuthSecret, error) {
result := s.db.Create(&authSecret)
return authSecret, CheckError(result)
func (s *BloodhoundDB) CreateAuthSecret(ctx context.Context, authSecret model.AuthSecret) (model.AuthSecret, error) {
auditEntry := model.AuditEntry{
Action: "DeleteAuthToken",
Model: &authSecret,
}

return authSecret, s.AuditableTransaction(ctx, auditEntry, func(tx *gorm.DB) error {
return CheckError(tx.Create(&authSecret))
})
}

// GetAuthSecret retrieves the AuthSecret row associated with the provided ID
Expand All @@ -544,9 +558,15 @@ func (s *BloodhoundDB) GetAuthSecret(id int32) (model.AuthSecret, error) {
// UpdateAuthSecret updates the auth secret with the input struct specified
// UPDATE auth_secrets SET digest = .., hmac_method = ..., expires_at = ...
// WHERE user_id = ....
func (s *BloodhoundDB) UpdateAuthSecret(authSecret model.AuthSecret) error {
result := s.db.Save(&authSecret)
return CheckError(result)
func (s *BloodhoundDB) UpdateAuthSecret(ctx context.Context, authSecret model.AuthSecret) error {
auditEntry := model.AuditEntry{
Action: "UpdateAuthSecret",
Model: &authSecret,
}

return s.AuditableTransaction(ctx, auditEntry, func(tx *gorm.DB) error {
return CheckError(tx.Save(&authSecret))
})
}

// DeleteAuthSecret deletes the auth secret row corresponding to the struct specified
Expand Down
10 changes: 6 additions & 4 deletions cmd/api/src/database/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ func TestDatabase_CreateGetUser(t *testing.T) {

func TestDatabase_CreateGetDeleteAuthToken(t *testing.T) {
var (
ctx = context.Background()
dbInst, user = initAndCreateUser(t)
expectedName = "test"
token = model.AuthToken{
Expand All @@ -251,7 +252,7 @@ func TestDatabase_CreateGetDeleteAuthToken(t *testing.T) {
}
)

if newToken, err := dbInst.CreateAuthToken(token); err != nil {
if newToken, err := dbInst.CreateAuthToken(ctx, token); err != nil {
t.Fatalf("Failed to create auth token: %v", err)
} else if updatedUser, err := dbInst.GetUser(user.ID); err != nil {
t.Fatalf("Failed to fetch updated user: %v", err)
Expand All @@ -261,7 +262,7 @@ func TestDatabase_CreateGetDeleteAuthToken(t *testing.T) {
t.Fatalf("Expected auth token to have valid name")
} else if newToken.Name.String != expectedName {
t.Fatalf("Expected auth token to have name %s but saw %v", expectedName, newToken.Name.String)
} else if err := dbInst.DeleteAuthToken(newToken); err != nil {
} else if err := dbInst.DeleteAuthToken(ctx, newToken); err != nil {
t.Fatalf("Failed to delete auth token: %v", err)
}

Expand All @@ -276,6 +277,7 @@ func TestDatabase_CreateGetDeleteAuthSecret(t *testing.T) {
const updatedDigest = "updated"

var (
ctx = context.Background()
dbInst, user = initAndCreateUser(t)
secret = model.AuthSecret{
UserID: user.ID,
Expand All @@ -285,7 +287,7 @@ func TestDatabase_CreateGetDeleteAuthSecret(t *testing.T) {
}
)

if newSecret, err := dbInst.CreateAuthSecret(secret); err != nil {
if newSecret, err := dbInst.CreateAuthSecret(ctx, secret); err != nil {
t.Fatalf("Failed to create auth secret: %v", err)
} else if updatedUser, err := dbInst.GetUser(user.ID); err != nil {
t.Fatalf("Failed to fetch updated user: %v", err)
Expand All @@ -294,7 +296,7 @@ func TestDatabase_CreateGetDeleteAuthSecret(t *testing.T) {
} else {
newSecret.Digest = updatedDigest

if err := dbInst.UpdateAuthSecret(newSecret); err != nil {
if err := dbInst.UpdateAuthSecret(ctx, newSecret); err != nil {
t.Fatalf("Failed to update auth secret %d: %v", newSecret.ID, err)
} else if updatedSecret, err := dbInst.GetAuthSecret(newSecret.ID); err != nil {
t.Fatalf("Failed to fetch updated auth secret: %v", err)
Expand Down
8 changes: 4 additions & 4 deletions cmd/api/src/database/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,16 +106,16 @@ type Database interface {
GetUser(id uuid.UUID) (model.User, error)
DeleteUser(user model.User) error
LookupUser(principalName string) (model.User, error)
CreateAuthToken(authToken model.AuthToken) (model.AuthToken, error)
CreateAuthToken(ctx context.Context, authToken model.AuthToken) (model.AuthToken, error)
UpdateAuthToken(authToken model.AuthToken) error
GetAllAuthTokens(order string, filter model.SQLFilter) (model.AuthTokens, error)
GetAuthToken(id uuid.UUID) (model.AuthToken, error)
ListUserTokens(userID uuid.UUID, order string, filter model.SQLFilter) (model.AuthTokens, error)
GetUserToken(userId, tokenId uuid.UUID) (model.AuthToken, error)
DeleteAuthToken(authToken model.AuthToken) error
CreateAuthSecret(authSecret model.AuthSecret) (model.AuthSecret, error)
DeleteAuthToken(ctx context.Context, authToken model.AuthToken) error
CreateAuthSecret(ctx context.Context, authSecret model.AuthSecret) (model.AuthSecret, error)
GetAuthSecret(id int32) (model.AuthSecret, error)
UpdateAuthSecret(authSecret model.AuthSecret) error
UpdateAuthSecret(ctx context.Context, authSecret model.AuthSecret) error
DeleteAuthSecret(authSecret model.AuthSecret) error
CreateSAMLIdentityProvider(samlProvider model.SAMLProvider) (model.SAMLProvider, error)
UpdateSAMLIdentityProvider(samlProvider model.SAMLProvider) error
Expand Down
32 changes: 16 additions & 16 deletions cmd/api/src/database/mocks/db.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions cmd/api/src/model/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,16 @@ type AuthToken struct {
Unique
}

func (s AuthToken) AuditData() AuditData {
return AuditData{
"id": s.ID,
"user_id": s.UserID,
"client_id": s.ClientID,
"name": s.Name,
"last_access": s.LastAccess,
}
}

func (s AuthToken) StripKey() AuthToken {
return AuthToken{
UserID: s.UserID,
Expand Down

0 comments on commit e8c6158

Please sign in to comment.