Security fixes + project files: Session fixation, SameSite cookies, audit log redaction, backup integrity, Docker hardening, error sanitization, rate limiting + pre-existing test and UI fixes
This commit is contained in:
parent
b587fb87a9
commit
acd95a0891
7 changed files with 874 additions and 16 deletions
|
|
@ -79,7 +79,8 @@ func newTestRouter(t *testing.T, db *sql.DB) (*gin.Engine, *file.Service) {
|
|||
}
|
||||
t.Cleanup(func() { os.RemoveAll(dir) })
|
||||
// also ensure backups dir is created by service; we use filepath join
|
||||
svc, err := file.NewService(filepath.Join(dir, "dynamic"), db)
|
||||
auditRepo := repositories.NewAuditRepository(db)
|
||||
svc, err := file.NewService(filepath.Join(dir, "dynamic"), db, auditRepo)
|
||||
if err != nil {
|
||||
t.Fatalf("service: %v", err)
|
||||
}
|
||||
|
|
@ -87,7 +88,7 @@ func newTestRouter(t *testing.T, db *sql.DB) (*gin.Engine, *file.Service) {
|
|||
sessionRepo := repositories.NewSessionRepository(db)
|
||||
authMw := middleware.NewAuthMiddleware(sessionRepo, userRepo)
|
||||
authHandler := NewAuthHandler(userRepo, sessionRepo, "test-secret-32-chars-minimum-length", "", false)
|
||||
fileHandler := NewFileConfigHandler(svc)
|
||||
fileHandler := NewFileConfigHandler(svc, auditRepo, userRepo)
|
||||
|
||||
r := gin.New()
|
||||
r.Use(middleware.CORSMiddleware("http://localhost:5173"))
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"testing"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/traefik/traefik-gui/backend/internal/database/repositories"
|
||||
)
|
||||
|
||||
func newTestService(t *testing.T) (*Service, string, func()) {
|
||||
|
|
@ -32,7 +33,8 @@ func newTestService(t *testing.T) (*Service, string, func()) {
|
|||
if err != nil {
|
||||
t.Fatalf("create backups: %v", err)
|
||||
}
|
||||
svc, err := NewService(filepath.Join(dir, "dynamic"), db)
|
||||
auditRepo := repositories.NewAuditRepository(db)
|
||||
svc, err := NewService(filepath.Join(dir, "dynamic"), db, auditRepo)
|
||||
if err != nil {
|
||||
t.Fatalf("new service: %v", err)
|
||||
}
|
||||
|
|
|
|||
138
backend/internal/database/repositories/audit.go
Normal file
138
backend/internal/database/repositories/audit.go
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
package repositories
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AuditLogEntry struct {
|
||||
ID string
|
||||
UserID string
|
||||
Username string
|
||||
Role string
|
||||
Action string
|
||||
ResourceType string
|
||||
ResourceName string
|
||||
Provider string
|
||||
SourceFile string
|
||||
ContentHash string
|
||||
Timestamp time.Time
|
||||
Result string
|
||||
ErrorCategory string
|
||||
RollbackOccurred bool
|
||||
ErrorMessage string
|
||||
}
|
||||
|
||||
type AuditRepository struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewAuditRepository(db *sql.DB) *AuditRepository {
|
||||
return &AuditRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *AuditRepository) Create(entry *AuditLogEntry) error {
|
||||
_, err := r.db.Exec(
|
||||
`INSERT INTO audit_log (id, user_id, username, role, action, resource_type, resource_name, provider, source_file, content_hash, timestamp, result, error_category, rollback_occurred, error_message)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
entry.ID, entry.UserID, entry.Username, entry.Role, entry.Action, entry.ResourceType, entry.ResourceName,
|
||||
entry.Provider, entry.SourceFile, entry.ContentHash, entry.Timestamp, entry.Result, entry.ErrorCategory,
|
||||
entry.RollbackOccurred, entry.ErrorMessage,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *AuditRepository) List(opts ListAuditOptions) ([]AuditLogEntry, int, error) {
|
||||
query := `SELECT id, user_id, username, role, action, resource_type, resource_name, provider, source_file, content_hash, timestamp, result, error_category, rollback_occurred, error_message FROM audit_log WHERE 1=1 `
|
||||
var args []interface{}
|
||||
argIdx := 1
|
||||
|
||||
if opts.Username != "" {
|
||||
// Look up user ID by username
|
||||
var userID string
|
||||
err := r.db.QueryRow(`SELECT id FROM users WHERE username = ?`, opts.Username).Scan(&userID)
|
||||
if err == nil && userID != "" {
|
||||
opts.UserID = userID
|
||||
}
|
||||
// If user not found, we just won't filter by user_id (entries without a matching user_id will be excluded)
|
||||
}
|
||||
if opts.UserID != "" {
|
||||
query += " AND user_id=? "
|
||||
args = append(args, opts.UserID)
|
||||
argIdx++
|
||||
}
|
||||
if opts.Action != "" {
|
||||
query += " AND action=? "
|
||||
args = append(args, opts.Action)
|
||||
argIdx++
|
||||
}
|
||||
if opts.Result != "" {
|
||||
query += " AND result=? "
|
||||
args = append(args, opts.Result)
|
||||
argIdx++
|
||||
}
|
||||
if opts.ResourceType != "" {
|
||||
query += " AND resource_type=? "
|
||||
args = append(args, opts.ResourceType)
|
||||
argIdx++
|
||||
}
|
||||
if opts.StartDate != "" {
|
||||
query += " AND timestamp>=? "
|
||||
args = append(args, opts.StartDate)
|
||||
argIdx++
|
||||
}
|
||||
if opts.EndDate != "" {
|
||||
query += " AND timestamp<=? "
|
||||
args = append(args, opts.EndDate)
|
||||
argIdx++
|
||||
}
|
||||
|
||||
countQuery := query + " LIMIT 1 "
|
||||
var count int
|
||||
err := r.db.QueryRow(countQuery, args...).Scan(&count)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
query += " ORDER BY timestamp DESC LIMIT ? OFFSET ?"
|
||||
args = append(args, opts.Limit, opts.Offset)
|
||||
|
||||
rows, err := r.db.Query(query, args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var entries []AuditLogEntry
|
||||
for rows.Next() {
|
||||
var e AuditLogEntry
|
||||
var rollbackOccurred int
|
||||
err := rows.Scan(&e.ID, &e.UserID, &e.Username, &e.Role, &e.Action, &e.ResourceType, &e.ResourceName,
|
||||
&e.Provider, &e.SourceFile, &e.ContentHash, &e.Timestamp, &e.Result, &e.ErrorCategory, &rollbackOccurred, &e.ErrorMessage)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
e.RollbackOccurred = rollbackOccurred != 0
|
||||
entries = append(entries, e)
|
||||
}
|
||||
return entries, count, nil
|
||||
}
|
||||
|
||||
type ListAuditOptions struct {
|
||||
UserID string
|
||||
Username string
|
||||
Action string
|
||||
Result string
|
||||
ResourceType string
|
||||
StartDate string
|
||||
EndDate string
|
||||
Limit int
|
||||
Offset int
|
||||
}
|
||||
|
||||
func defaultListOptions() ListAuditOptions {
|
||||
return ListAuditOptions{
|
||||
Limit: 50,
|
||||
Offset: 0,
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue