Skip to content

Commit

Permalink
Merge pull request teamhanko#490 from teamhanko/feat-audit-log-search…
Browse files Browse the repository at this point in the history
…able

feat: add query parameter for searching audit logs
  • Loading branch information
FreddyDevelop authored Jan 17, 2023
2 parents f5e9173 + c934bad commit 93ad9c4
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 42 deletions.
17 changes: 11 additions & 6 deletions backend/handler/audit_log.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,15 @@ func NewAuditLogHandler(persister persistence.Persister) *AuditLogHandler {
}

type AuditLogListRequest struct {
Page int `query:"page"`
PerPage int `query:"per_page"`
StartTime *time.Time `query:"start_time"`
EndTime *time.Time `query:"end_time"`
Page int `query:"page"`
PerPage int `query:"per_page"`
StartTime *time.Time `query:"start_time"`
EndTime *time.Time `query:"end_time"`
Types []string `query:"type"`
UserId string `query:"actor_user_id"`
Email string `query:"actor_email"`
IP string `query:"meta_source_ip"`
SearchString string `query:"q"`
}

func (h AuditLogHandler) List(c echo.Context) error {
Expand All @@ -44,12 +49,12 @@ func (h AuditLogHandler) List(c echo.Context) error {
request.PerPage = 20
}

auditLogs, err := h.persister.GetAuditLogPersister().List(request.Page, request.PerPage, request.StartTime, request.EndTime)
auditLogs, err := h.persister.GetAuditLogPersister().List(request.Page, request.PerPage, request.StartTime, request.EndTime, request.Types, request.UserId, request.Email, request.IP, request.SearchString)
if err != nil {
return fmt.Errorf("failed to get list of audit logs: %w", err)
}

logCount, err := h.persister.GetAuditLogPersister().Count(request.StartTime, request.EndTime)
logCount, err := h.persister.GetAuditLogPersister().Count(request.StartTime, request.EndTime, request.Types, request.UserId, request.Email, request.IP, request.SearchString)
if err != nil {
return fmt.Errorf("failed to get total count of audit logs: %w", err)
}
Expand Down
63 changes: 49 additions & 14 deletions backend/persistence/audit_log_persister.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@ import (
"github.com/gobuffalo/pop/v6"
"github.com/gofrs/uuid"
"github.com/teamhanko/hanko/backend/persistence/models"
"strings"
"time"
)

type AuditLogPersister interface {
Create(auditLog models.AuditLog) error
Get(id uuid.UUID) (*models.AuditLog, error)
List(page int, perPage int, startTime *time.Time, endTime *time.Time) ([]models.AuditLog, error)
List(page int, perPage int, startTime *time.Time, endTime *time.Time, types []string, userId string, email string, ip string, searchString string) ([]models.AuditLog, error)
Delete(auditLog models.AuditLog) error
Count(startTime *time.Time, endTime *time.Time) (int, error)
Count(startTime *time.Time, endTime *time.Time, types []string, userId string, email string, ip string, searchString string) (int, error)
}

type auditLogPersister struct {
Expand Down Expand Up @@ -51,16 +52,11 @@ func (p *auditLogPersister) Get(id uuid.UUID) (*models.AuditLog, error) {
return &auditLog, nil
}

func (p *auditLogPersister) List(page int, perPage int, startTime *time.Time, endTime *time.Time) ([]models.AuditLog, error) {
func (p *auditLogPersister) List(page int, perPage int, startTime *time.Time, endTime *time.Time, types []string, userId string, email string, ip string, searchString string) ([]models.AuditLog, error) {
auditLogs := []models.AuditLog{}

query := p.db.Q()
if startTime != nil {
query = query.Where("created_at > ?", startTime)
}
if endTime != nil {
query = query.Where("created_at < ?", endTime)
}
query = p.addQueryParamsToSqlQuery(query, startTime, endTime, types, userId, email, ip, searchString)
err := query.Paginate(page, perPage).Order("created_at desc").All(&auditLogs)

if err != nil && errors.Is(err, sql.ErrNoRows) {
Expand All @@ -82,18 +78,57 @@ func (p *auditLogPersister) Delete(auditLog models.AuditLog) error {
return nil
}

func (p *auditLogPersister) Count(startTime *time.Time, endTime *time.Time) (int, error) {
func (p *auditLogPersister) Count(startTime *time.Time, endTime *time.Time, types []string, userId string, email string, ip string, searchString string) (int, error) {
query := p.db.Q()
query = p.addQueryParamsToSqlQuery(query, startTime, endTime, types, userId, email, ip, searchString)
count, err := query.Count(&models.AuditLog{})
if err != nil {
return 0, fmt.Errorf("failed to get auditLog count: %w", err)
}

return count, nil
}

func (p *auditLogPersister) addQueryParamsToSqlQuery(query *pop.Query, startTime *time.Time, endTime *time.Time, types []string, userId string, email string, ip string, searchString string) *pop.Query {
if startTime != nil {
query = query.Where("created_at > ?", startTime)
}
if endTime != nil {
query = query.Where("created_at < ?", endTime)
}
count, err := query.Count(&models.AuditLog{})
if err != nil {
return 0, fmt.Errorf("failed to get auditLog count: %w", err)

if len(types) > 0 {
joined := "'" + strings.Join(types, "','") + "'"
query = query.Where(fmt.Sprintf("type IN (%s)", joined))
}

return count, nil
if len(userId) > 0 {
switch p.db.Dialect.Name() {
case "postgres", "cockroach":
query = query.Where("actor_user_id::text LIKE ?", "%"+userId+"%")
case "mysql", "mariadb":
query = query.Where("actor_user_id LIKE ?", "%"+userId+"%")
}
}

if len(email) > 0 {
query = query.Where("actor_email LIKE ?", "%"+email+"%")
}

if len(ip) > 0 {
query = query.Where("meta_source_ip LIKE ?", "%"+ip+"%")
}

if len(searchString) > 0 {
switch p.db.Dialect.Name() {
case "postgres", "cockroach":
arg := "%" + searchString + "%"
query = query.Where("(actor_email LIKE ? OR meta_source_ip LIKE ? OR actor_user_id::text LIKE ?)", arg, arg, arg)
case "mysql", "mariadb":
arg := "%" + searchString + "%"
query = query.Where("(actor_email LIKE ? OR meta_source_ip LIKE ? OR actor_user_id LIKE ?)", arg, arg, arg)
}
}

return query
}
4 changes: 2 additions & 2 deletions backend/test/audit_log_persister.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func (p *auditLogPersister) Get(id uuid.UUID) (*models.AuditLog, error) {
return found, nil
}

func (p *auditLogPersister) List(page int, perPage int, startTime *time.Time, endTime *time.Time) ([]models.AuditLog, error) {
func (p *auditLogPersister) List(page int, perPage int, startTime *time.Time, endTime *time.Time, types []string, userId string, email string, ip string, searchString string) ([]models.AuditLog, error) {
if len(p.logs) == 0 {
return p.logs, nil
}
Expand Down Expand Up @@ -76,6 +76,6 @@ func (p *auditLogPersister) Delete(auditLog models.AuditLog) error {
return nil
}

func (p *auditLogPersister) Count(startTime *time.Time, endTime *time.Time) (int, error) {
func (p *auditLogPersister) Count(startTime *time.Time, endTime *time.Time, types []string, userId string, email string, ip string, searchString string) (int, error) {
return len(p.logs), nil
}
84 changes: 64 additions & 20 deletions docs/static/spec/admin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,47 @@ paths:
type: string
example: 2022-09-15T12:48:48Z
description: Date and time to which the logs are included
- in: query
name: actor_user_id
schema:
allOf:
- $ref: '#/components/schemas/UUID4'
example: c339547d-e17d-4ba7-8a1d-b3d5a4d17c1c
description: Only audit logs with the specified user_id are included
- in: query
name: actor_email
schema:
type: string
format: email
example: example@example.com
description: Only audit logs with the specified email are included
- in: query
name: meta_source_ip
schema:
allOf:
- type: string
format: ipv4
- type: string
format: ipv6
example: 127.0.0.1
description: Only audit logs with the specified ip address are included
- in: query
name: q
schema:
type: string
example: example.com
description: Only audit logs are included when the search string matches values in meta_source_ip or actor_user_id or actor_email
- in: query
name: type
schema:
type: array
items:
allOf:
- $ref: '#/components/schemas/AuditLogTypes'
example: user_created
style: form
explode: true
description: Only audit logs with the specified type are included
responses:
'200':
description: 'Details about audit logs'
Expand Down Expand Up @@ -234,26 +275,8 @@ components:
allOf:
- $ref: '#/components/schemas/UUID4'
type:
description: The type of the audit log
type: string
enum:
- user_created
- password_set_succeeded
- password_set_failed
- password_login_succeeded
- password_login_failed
- passcode_login_init_succeeded
- passcode_login_init_failed
- passcode_login_final_succeeded
- passcode_login_final_failed
- webauthn_registration_init_succeeded
- webauthn_registration_init_failed
- webauthn_registration_final_succeeded
- webauthn_registration_final_failed
- webauthn_authentication_init_succeeded
- webauthn_authentication_init_failed
- webauthn_authentication_final_succeeded
- webauthn_authentication_final_failed
allOf:
- $ref: '#/components/schemas/AuditLogTypes'
error:
description: A more detailed message why something failed
type: string
Expand Down Expand Up @@ -302,6 +325,27 @@ components:
format: int32
message:
type: string
AuditLogTypes:
description: The type of the audit log
type: string
enum:
- user_created
- password_set_succeeded
- password_set_failed
- password_login_succeeded
- password_login_failed
- passcode_login_init_succeeded
- passcode_login_init_failed
- passcode_login_final_succeeded
- passcode_login_final_failed
- webauthn_registration_init_succeeded
- webauthn_registration_init_failed
- webauthn_registration_final_succeeded
- webauthn_registration_final_failed
- webauthn_authentication_init_succeeded
- webauthn_authentication_init_failed
- webauthn_authentication_final_succeeded
- webauthn_authentication_final_failed
headers:
X-Total-Count:
schema:
Expand Down

0 comments on commit 93ad9c4

Please sign in to comment.