Implement audit logging functionality across services and middleware
- Introduced a new `auditlog` package to handle audit logging for user actions, including creation, updates, deletions, and authentication events. - Enhanced existing services (customer, user) to log relevant actions using the new audit logger, capturing details such as actor ID, action type, target type, and success status. - Added middleware to enrich request context with metadata for audit logging, ensuring comprehensive tracking of user interactions. - Integrated Elasticsearch for persistent storage of audit logs, with fallback to file-only logging if Elasticsearch is unavailable. - Updated API documentation to include new audit log endpoints for administrative access. This update significantly improves the system's ability to track and audit user actions, enhancing security and accountability within the application.
This commit is contained in:
@@ -4,8 +4,10 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
ai_summarizer "tm/pkg/ai_summarizer"
|
||||
"tm/pkg/audit"
|
||||
"tm/pkg/authorization"
|
||||
"tm/pkg/config"
|
||||
"tm/pkg/elasticsearch"
|
||||
"tm/pkg/filestore"
|
||||
"tm/pkg/gorules"
|
||||
"tm/pkg/hcaptcha"
|
||||
@@ -90,6 +92,7 @@ func InitHTTPServer(conf Config, log logger.Logger) *echo.Echo {
|
||||
// Add middleware
|
||||
e.Use(middleware.Recover())
|
||||
e.Use(middleware.RequestID())
|
||||
e.Use(audit.RequestMetaMiddleware())
|
||||
e.Use(middleware.Logger())
|
||||
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
|
||||
AllowOrigins: []string{"*"},
|
||||
@@ -132,7 +135,7 @@ func InitHTTPServer(conf Config, log logger.Logger) *echo.Echo {
|
||||
e.GET("/swagger*", echoSwagger.WrapHandler)
|
||||
|
||||
log.Info("HTTP server initialized successfully", map[string]interface{}{
|
||||
"middleware": []string{"recover", "request_id", "logger", "cors", "custom_logging"},
|
||||
"middleware": []string{"recover", "request_id", "audit_meta", "logger", "cors", "custom_logging"},
|
||||
})
|
||||
|
||||
return e
|
||||
@@ -402,3 +405,34 @@ func InitGoRulesClient(_ GoRulesConfig, log logger.Logger) gorules.Client {
|
||||
return client
|
||||
*/
|
||||
}
|
||||
|
||||
// InitElasticsearch creates an Elasticsearch client for audit log storage.
|
||||
func InitElasticsearch(conf config.ElasticsearchConfig, log logger.Logger) elasticsearch.Client {
|
||||
esConfig := elasticsearch.Config{
|
||||
URL: conf.URL,
|
||||
Username: conf.Username,
|
||||
Password: conf.Password,
|
||||
IndexPrefix: conf.IndexPrefix,
|
||||
Enabled: conf.Enabled,
|
||||
RequestTimeout: conf.RequestTimeout,
|
||||
BulkFlushPeriod: conf.BulkFlushPeriod,
|
||||
QueueSize: conf.QueueSize,
|
||||
}
|
||||
|
||||
client, err := elasticsearch.NewClient(esConfig, log)
|
||||
if err != nil {
|
||||
log.Warn("Elasticsearch unavailable, audit logs will be file-only", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"url": conf.URL,
|
||||
})
|
||||
return elasticsearch.NewNoopClient()
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
// InitAuditLogger creates a composite audit logger with optional Elasticsearch persistence.
|
||||
func InitAuditLogger(log logger.Logger, esClient elasticsearch.Client) (audit.Logger, audit.Store) {
|
||||
store := elasticsearch.NewAuditStore(esClient)
|
||||
return audit.NewCompositeLogger(log, store), store
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ type Config struct {
|
||||
DocumentScraper DocumentScraperConfig
|
||||
AISummarizer AISummarizerConfig
|
||||
GoRules GoRulesConfig
|
||||
Elasticsearch config.ElasticsearchConfig
|
||||
}
|
||||
|
||||
type ScraperConfig struct {
|
||||
|
||||
+29
-6
@@ -62,6 +62,9 @@ package main
|
||||
// @tag.name Admin-AI-Pipeline
|
||||
// @tag.description Administrative Opplens AI pipeline operations including scrape, summarize, translate, sync, and background pipeline runs
|
||||
|
||||
// @tag.name Admin-AuditLogs
|
||||
// @tag.description Administrative user action audit log search with filtering by actor, action, target, and time range
|
||||
|
||||
// @tag.name Authorization
|
||||
// @tag.description Customer authentication and authorization operations for mobile application including login, logout, token refresh, and profile access
|
||||
|
||||
@@ -100,8 +103,9 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
"tm/internal/assets"
|
||||
"tm/internal/ai_pipeline"
|
||||
"tm/internal/assets"
|
||||
"tm/internal/auditlog"
|
||||
"tm/internal/cms"
|
||||
"tm/internal/company"
|
||||
"tm/internal/company_category"
|
||||
@@ -116,7 +120,6 @@ import (
|
||||
"tm/internal/tender"
|
||||
"tm/internal/tender_approval"
|
||||
"tm/internal/user"
|
||||
"tm/pkg/audit"
|
||||
"tm/pkg/filestore"
|
||||
"tm/pkg/security"
|
||||
|
||||
@@ -217,7 +220,24 @@ func main() {
|
||||
_ = cms.NewValidationService() // Register mediaRef validator
|
||||
|
||||
// Initialize services with repositories
|
||||
auditLogger := audit.NewLogger(logger)
|
||||
esClient := bootstrap.InitElasticsearch(conf.Elasticsearch, logger)
|
||||
defer func() {
|
||||
if err := esClient.Close(); err != nil {
|
||||
logger.Error("Failed to close Elasticsearch client", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
auditLogger, auditStore := bootstrap.InitAuditLogger(logger, esClient)
|
||||
defer func() {
|
||||
if err := auditStore.Close(); err != nil {
|
||||
logger.Error("Failed to close audit store", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
userService := user.NewService(userRepository, logger, userAuthService, userValidator, notificationSDK, redisClient, auditLogger)
|
||||
categoryService := company_category.NewService(categoryRepository, logger)
|
||||
companyService := company.NewService(companyRepository, categoryService, fileStoreService, aiRecommendationClient, redisClient, conf.AISummarizer.RecommendationCacheTTL, logger)
|
||||
@@ -235,8 +255,10 @@ func main() {
|
||||
dashboardRepository := dashboard.NewRepository(mongoManager, logger, procedureDocumentsLister)
|
||||
dashboardService := dashboard.NewService(dashboardRepository, logger)
|
||||
aiPipelineService := ai_pipeline.NewService(aiPipelineClient, logger, tenderService)
|
||||
auditLogRepository := auditlog.NewRepository(auditStore)
|
||||
auditLogService := auditlog.NewService(auditLogRepository, logger)
|
||||
logger.Info("Services initialized successfully", map[string]interface{}{
|
||||
"services": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "flag", "notification", "contact", "cms", "scraper", "kanban", "document_scraper", "dashboard", "ai_pipeline"},
|
||||
"services": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "flag", "notification", "contact", "cms", "scraper", "kanban", "document_scraper", "dashboard", "ai_pipeline", "auditlog"},
|
||||
})
|
||||
|
||||
// Initialize handlers with services
|
||||
@@ -257,8 +279,9 @@ func main() {
|
||||
documentScraperHandler := document_scraper.NewHandler(documentScraperService, logger)
|
||||
dashboardHandler := dashboard.NewHandler(dashboardService)
|
||||
aiPipelineHandler := ai_pipeline.NewHandler(aiPipelineService)
|
||||
auditLogHandler := auditlog.NewHandler(auditLogService)
|
||||
logger.Info("Handlers initialized successfully", map[string]interface{}{
|
||||
"handlers": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "flag", "notification", "contact", "cms", "scraper", "kanban", "filestore", "document_scraper", "dashboard", "ai_pipeline"},
|
||||
"handlers": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "flag", "notification", "contact", "cms", "scraper", "kanban", "filestore", "document_scraper", "dashboard", "ai_pipeline", "auditlog"},
|
||||
})
|
||||
|
||||
// Initialize HTTP server
|
||||
@@ -267,7 +290,7 @@ func main() {
|
||||
router.SetupCSPSecurity(e)
|
||||
|
||||
// Register routes
|
||||
router.RegisterAdminRoutes(e, userHandler, companyHandler, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, inquiryHandler, categoryHandler, flagHandler, notificationHandler, contactHandler, cmsHandler, kanbanHandler, fileStoreHandler, dashboardHandler, aiPipelineHandler, xssPolicy)
|
||||
router.RegisterAdminRoutes(e, userHandler, companyHandler, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, inquiryHandler, categoryHandler, flagHandler, notificationHandler, contactHandler, cmsHandler, kanbanHandler, fileStoreHandler, dashboardHandler, aiPipelineHandler, auditLogHandler, xssPolicy)
|
||||
router.RegisterPublicRoutes(e, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, companyHandler, inquiryHandler, categoryHandler, flagHandler, notificationHandler, contactHandler, cmsHandler, kanbanHandler, fileStoreHandler, xssPolicy)
|
||||
router.RegisterDocumentScraperRoutes(e, documentScraperHandler, conf.DocumentScraper.APIKey)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package router
|
||||
import (
|
||||
"tm/internal/assets"
|
||||
"tm/internal/ai_pipeline"
|
||||
"tm/internal/auditlog"
|
||||
"tm/internal/cms"
|
||||
"tm/internal/company"
|
||||
"tm/internal/company_category"
|
||||
@@ -35,7 +36,7 @@ func SetupCSPSecurity(e *echo.Echo) {
|
||||
e.POST("/api/v1/csp-report", security.CSPReportHandler)
|
||||
}
|
||||
|
||||
func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler *company.Handler, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler, flagHandler *assets.Handler, notificationHandler *notification.NotificationHandler, contactHandler *contact.Handler, cmsHandler *cms.Handler, kanbanHandler *kanban.Handler, fileStoreHandler *filestore.Handler, dashboardHandler *dashboard.Handler, aiPipelineHandler *ai_pipeline.Handler, xssPolicy *security.XSSPolicy) {
|
||||
func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler *company.Handler, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler, flagHandler *assets.Handler, notificationHandler *notification.NotificationHandler, contactHandler *contact.Handler, cmsHandler *cms.Handler, kanbanHandler *kanban.Handler, fileStoreHandler *filestore.Handler, dashboardHandler *dashboard.Handler, aiPipelineHandler *ai_pipeline.Handler, auditLogHandler *auditlog.Handler, xssPolicy *security.XSSPolicy) {
|
||||
adminV1 := e.Group("/admin/v1")
|
||||
|
||||
// Admin Users Routes
|
||||
@@ -51,6 +52,13 @@ func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler
|
||||
adminUsersGP.POST("/:id/reset-password", userHandler.ResetUserPassword, userHandler.AdminMiddleware())
|
||||
}
|
||||
|
||||
// Admin Audit Logs Routes
|
||||
auditLogsGP := adminV1.Group("/audit-logs")
|
||||
{
|
||||
auditLogsGP.Use(userHandler.AuthMiddleware(), userHandler.AdminMiddleware())
|
||||
auditLogsGP.GET("", auditLogHandler.Search)
|
||||
}
|
||||
|
||||
// Admin user profile
|
||||
profileGP := adminV1.Group("/profile")
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user