From cdee91d7dabfb520755fc7eb105aab5b04fe5d3a Mon Sep 17 00:00:00 2001 From: Frederic Jahn Date: Thu, 12 Jan 2023 17:28:09 +0100 Subject: [PATCH 1/2] feat: add query parameter for searching audit logs --- backend/handler/audit_log.go | 17 +++-- backend/persistence/audit_log_persister.go | 63 ++++++++++++---- docs/static/spec/admin.yaml | 84 ++++++++++++++++------ 3 files changed, 124 insertions(+), 40 deletions(-) diff --git a/backend/handler/audit_log.go b/backend/handler/audit_log.go index e831afd33..cc455b1d7 100644 --- a/backend/handler/audit_log.go +++ b/backend/handler/audit_log.go @@ -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 { @@ -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) } diff --git a/backend/persistence/audit_log_persister.go b/backend/persistence/audit_log_persister.go index 3bb1fe4c8..018e4f896 100644 --- a/backend/persistence/audit_log_persister.go +++ b/backend/persistence/audit_log_persister.go @@ -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 { @@ -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) { @@ -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 } diff --git a/docs/static/spec/admin.yaml b/docs/static/spec/admin.yaml index 8ada3a8e3..b20c7e925 100644 --- a/docs/static/spec/admin.yaml +++ b/docs/static/spec/admin.yaml @@ -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' @@ -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 @@ -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: From c934badea171142f0cf608ecd6533df61a16b062 Mon Sep 17 00:00:00 2001 From: Frederic Jahn Date: Fri, 13 Jan 2023 10:20:55 +0100 Subject: [PATCH 2/2] test: fix tests --- backend/test/audit_log_persister.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/test/audit_log_persister.go b/backend/test/audit_log_persister.go index bf3569cac..15d9d5605 100644 --- a/backend/test/audit_log_persister.go +++ b/backend/test/audit_log_persister.go @@ -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 } @@ -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 }