diff --git a/management/internals/modules/reverseproxy/accesslogs/filter.go b/management/internals/modules/reverseproxy/accesslogs/filter.go index 39b3fb677..5dccea235 100644 --- a/management/internals/modules/reverseproxy/accesslogs/filter.go +++ b/management/internals/modules/reverseproxy/accesslogs/filter.go @@ -3,6 +3,7 @@ package accesslogs import ( "net/http" "strconv" + "time" ) const ( @@ -18,21 +19,33 @@ type AccessLogFilter struct { Page int // PageSize is the number of records per page PageSize int + + // Filtering parameters + SourceIP *string // Filter by source IP address + Host *string // Filter by host header + Path *string // Filter by request path (supports LIKE pattern) + UserID *string // Filter by authenticated user ID + UserEmail *string // Filter by user email (requires user lookup) + UserName *string // Filter by user name (requires user lookup) + Method *string // Filter by HTTP method + StatusCode *int // Filter by HTTP status code + StartDate *time.Time // Filter by timestamp >= start_date + EndDate *time.Time // Filter by timestamp <= end_date } -// ParseFromRequest parses pagination parameters from HTTP request query parameters +// ParseFromRequest parses pagination and filter parameters from HTTP request query parameters func (f *AccessLogFilter) ParseFromRequest(r *http.Request) { - // Parse page number (default: 1) + queryParams := r.URL.Query() + f.Page = 1 - if pageStr := r.URL.Query().Get("page"); pageStr != "" { + if pageStr := queryParams.Get("page"); pageStr != "" { if page, err := strconv.Atoi(pageStr); err == nil && page > 0 { f.Page = page } } - // Parse page size (default: DefaultPageSize, max: MaxPageSize) f.PageSize = DefaultPageSize - if pageSizeStr := r.URL.Query().Get("page_size"); pageSizeStr != "" { + if pageSizeStr := queryParams.Get("page_size"); pageSizeStr != "" { if pageSize, err := strconv.Atoi(pageSizeStr); err == nil && pageSize > 0 { f.PageSize = pageSize if f.PageSize > MaxPageSize { @@ -40,6 +53,54 @@ func (f *AccessLogFilter) ParseFromRequest(r *http.Request) { } } } + + if sourceIP := queryParams.Get("source_ip"); sourceIP != "" { + f.SourceIP = &sourceIP + } + + if host := queryParams.Get("host"); host != "" { + f.Host = &host + } + + if path := queryParams.Get("path"); path != "" { + f.Path = &path + } + + if userID := queryParams.Get("user_id"); userID != "" { + f.UserID = &userID + } + + if userEmail := queryParams.Get("user_email"); userEmail != "" { + f.UserEmail = &userEmail + } + + if userName := queryParams.Get("user_name"); userName != "" { + f.UserName = &userName + } + + if method := queryParams.Get("method"); method != "" { + f.Method = &method + } + + if statusCodeStr := queryParams.Get("status_code"); statusCodeStr != "" { + if statusCode, err := strconv.Atoi(statusCodeStr); err == nil && statusCode > 0 { + f.StatusCode = &statusCode + } + } + + if startDate := queryParams.Get("start_date"); startDate != "" { + parsedStartDate, err := time.Parse(time.RFC3339, startDate) + if err == nil { + f.StartDate = &parsedStartDate + } + } + + if endDate := queryParams.Get("end_date"); endDate != "" { + parsedEndDate, err := time.Parse(time.RFC3339, endDate) + if err == nil { + f.EndDate = &parsedEndDate + } + } } // GetOffset calculates the database offset for pagination diff --git a/management/internals/modules/reverseproxy/accesslogs/interface.go b/management/internals/modules/reverseproxy/accesslogs/interface.go index af5b0c806..1c51a8a7d 100644 --- a/management/internals/modules/reverseproxy/accesslogs/interface.go +++ b/management/internals/modules/reverseproxy/accesslogs/interface.go @@ -6,5 +6,5 @@ import ( type Manager interface { SaveAccessLog(ctx context.Context, proxyLog *AccessLogEntry) error - GetAllAccessLogs(ctx context.Context, accountID, userID string, filter AccessLogFilter) ([]*AccessLogEntry, int64, error) + GetAllAccessLogs(ctx context.Context, accountID, userID string, filter *AccessLogFilter) ([]*AccessLogEntry, int64, error) } diff --git a/management/internals/modules/reverseproxy/accesslogs/manager/api.go b/management/internals/modules/reverseproxy/accesslogs/manager/api.go index 8c3bbdc39..1e1414ca5 100644 --- a/management/internals/modules/reverseproxy/accesslogs/manager/api.go +++ b/management/internals/modules/reverseproxy/accesslogs/manager/api.go @@ -30,11 +30,10 @@ func (h *handler) getAccessLogs(w http.ResponseWriter, r *http.Request) { return } - // Parse pagination parameters from request var filter accesslogs.AccessLogFilter filter.ParseFromRequest(r) - logs, totalCount, err := h.manager.GetAllAccessLogs(r.Context(), userAuth.AccountId, userAuth.UserId, filter) + logs, totalCount, err := h.manager.GetAllAccessLogs(r.Context(), userAuth.AccountId, userAuth.UserId, &filter) if err != nil { util.WriteError(r.Context(), err, w) return @@ -45,7 +44,6 @@ func (h *handler) getAccessLogs(w http.ResponseWriter, r *http.Request) { apiLogs = append(apiLogs, *log.ToAPIResponse()) } - // Return paginated response response := &api.ProxyAccessLogsResponse{ Data: apiLogs, Page: filter.Page, diff --git a/management/internals/modules/reverseproxy/accesslogs/manager/manager.go b/management/internals/modules/reverseproxy/accesslogs/manager/manager.go index 087069cc0..369b53547 100644 --- a/management/internals/modules/reverseproxy/accesslogs/manager/manager.go +++ b/management/internals/modules/reverseproxy/accesslogs/manager/manager.go @@ -2,6 +2,7 @@ package manager import ( "context" + "strings" log "github.com/sirupsen/logrus" @@ -55,8 +56,8 @@ func (m *managerImpl) SaveAccessLog(ctx context.Context, logEntry *accesslogs.Ac return nil } -// GetAllAccessLogs retrieves access logs for an account with pagination -func (m *managerImpl) GetAllAccessLogs(ctx context.Context, accountID, userID string, filter accesslogs.AccessLogFilter) ([]*accesslogs.AccessLogEntry, int64, error) { +// GetAllAccessLogs retrieves access logs for an account with pagination and filtering +func (m *managerImpl) GetAllAccessLogs(ctx context.Context, accountID, userID string, filter *accesslogs.AccessLogFilter) ([]*accesslogs.AccessLogEntry, int64, error) { ok, err := m.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Services, operations.Read) if err != nil { return nil, 0, status.NewPermissionValidationError(err) @@ -65,10 +66,43 @@ func (m *managerImpl) GetAllAccessLogs(ctx context.Context, accountID, userID st return nil, 0, status.NewPermissionDeniedError() } - logs, totalCount, err := m.store.GetAccountAccessLogs(ctx, store.LockingStrengthNone, accountID, filter) + if err := m.resolveUserFilters(ctx, accountID, filter); err != nil { + log.WithContext(ctx).Warnf("failed to resolve user filters: %v", err) + } + + logs, totalCount, err := m.store.GetAccountAccessLogs(ctx, store.LockingStrengthNone, accountID, *filter) if err != nil { return nil, 0, err } return logs, totalCount, nil } + +// resolveUserFilters converts user email/name filters to user ID filter +func (m *managerImpl) resolveUserFilters(ctx context.Context, accountID string, filter *accesslogs.AccessLogFilter) error { + if filter.UserEmail == nil && filter.UserName == nil { + return nil + } + + users, err := m.store.GetAccountUsers(ctx, store.LockingStrengthNone, accountID) + if err != nil { + return err + } + + var matchingUserIDs []string + for _, user := range users { + if filter.UserEmail != nil && strings.Contains(strings.ToLower(user.Email), strings.ToLower(*filter.UserEmail)) { + matchingUserIDs = append(matchingUserIDs, user.Id) + continue + } + if filter.UserName != nil && strings.Contains(strings.ToLower(user.Name), strings.ToLower(*filter.UserName)) { + matchingUserIDs = append(matchingUserIDs, user.Id) + } + } + + if len(matchingUserIDs) > 0 { + filter.UserID = &matchingUserIDs[0] + } + + return nil +} diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index c432ce7e3..beaa724ea 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -5061,24 +5061,28 @@ func (s *SqlStore) CreateAccessLog(ctx context.Context, logEntry *accesslogs.Acc return nil } -// GetAccountAccessLogs retrieves access logs for a given account with pagination +// GetAccountAccessLogs retrieves access logs for a given account with pagination and filtering func (s *SqlStore) GetAccountAccessLogs(ctx context.Context, lockStrength LockingStrength, accountID string, filter accesslogs.AccessLogFilter) ([]*accesslogs.AccessLogEntry, int64, error) { var logs []*accesslogs.AccessLogEntry var totalCount int64 - // Count total records for pagination metadata - countQuery := s.db.WithContext(ctx). + baseQuery := s.db.WithContext(ctx). Model(&accesslogs.AccessLogEntry{}). Where(accountIDCondition, accountID) - if err := countQuery.Count(&totalCount).Error; err != nil { + baseQuery = s.applyAccessLogFilters(baseQuery, filter) + + if err := baseQuery.Count(&totalCount).Error; err != nil { log.WithContext(ctx).Errorf("failed to count access logs: %v", err) return nil, 0, status.Errorf(status.Internal, "failed to count access logs") } - // Query with pagination query := s.db.WithContext(ctx). - Where(accountIDCondition, accountID). + Where(accountIDCondition, accountID) + + query = s.applyAccessLogFilters(query, filter) + + query = query. Order("timestamp DESC"). Limit(filter.GetLimit()). Offset(filter.GetOffset()) @@ -5096,6 +5100,44 @@ func (s *SqlStore) GetAccountAccessLogs(ctx context.Context, lockStrength Lockin return logs, totalCount, nil } +// applyAccessLogFilters applies filter conditions to the query +func (s *SqlStore) applyAccessLogFilters(query *gorm.DB, filter accesslogs.AccessLogFilter) *gorm.DB { + if filter.SourceIP != nil { + query = query.Where("source_ip = ?", *filter.SourceIP) + } + + if filter.Host != nil { + query = query.Where("host = ?", *filter.Host) + } + + if filter.Path != nil { + // Support LIKE pattern for path filtering + query = query.Where("path LIKE ?", "%"+*filter.Path+"%") + } + + if filter.UserID != nil { + query = query.Where("user_id = ?", *filter.UserID) + } + + if filter.Method != nil { + query = query.Where("method = ?", *filter.Method) + } + + if filter.StatusCode != nil { + query = query.Where("status_code = ?", *filter.StatusCode) + } + + if filter.StartDate != nil { + query = query.Where("timestamp >= ?", *filter.StartDate) + } + + if filter.EndDate != nil { + query = query.Where("timestamp <= ?", *filter.EndDate) + } + + return query +} + func (s *SqlStore) GetReverseProxyTargetByTargetID(ctx context.Context, lockStrength LockingStrength, accountID string, targetID string) (*reverseproxy.Target, error) { tx := s.db if lockStrength != LockingStrengthNone { diff --git a/shared/management/http/api/openapi.yml b/shared/management/http/api/openapi.yml index 644617f4f..d2790fd71 100644 --- a/shared/management/http/api/openapi.yml +++ b/shared/management/http/api/openapi.yml @@ -6389,6 +6389,61 @@ paths: minimum: 1 maximum: 100 description: Number of items per page (max 100) + - in: query + name: source_ip + schema: + type: string + description: Filter by source IP address + - in: query + name: host + schema: + type: string + description: Filter by host header + - in: query + name: path + schema: + type: string + description: Filter by request path (supports partial matching) + - in: query + name: user_id + schema: + type: string + description: Filter by authenticated user ID + - in: query + name: user_email + schema: + type: string + description: Filter by user email (partial matching) + - in: query + name: user_name + schema: + type: string + description: Filter by user name (partial matching) + - in: query + name: method + schema: + type: string + enum: [GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS] + description: Filter by HTTP method + - in: query + name: status_code + schema: + type: integer + minimum: 100 + maximum: 599 + description: Filter by HTTP status code + - in: query + name: start_date + schema: + type: string + format: date-time + description: Filter by timestamp >= start_date (RFC3339 format) + - in: query + name: end_date + schema: + type: string + format: date-time + description: Filter by timestamp <= end_date (RFC3339 format) responses: "200": description: Paginated list of reverse proxy access logs