diff --git a/src/db.go b/src/db.go index 1f474ba..0dd2ea2 100644 --- a/src/db.go +++ b/src/db.go @@ -155,7 +155,7 @@ func IsPermanentWhitelisted(db *sql.DB, ip string) (bool, error) { // AddTempEntry inserts or updates a temporary whitelist entry (upsert). func AddTempEntry(db *sql.DB, ip string, ttlSeconds int, reason, apiClientIP string) error { - expiresAt := time.Now().Add(time.Duration(ttlSeconds) * time.Second) + expiresAt := time.Now().Add(time.Duration(ttlSeconds) * time.Second).Format(time.RFC3339) _, err := db.Exec(` INSERT INTO temp_whitelist(ip, expires_at, reason) VALUES(?, ?, ?) @@ -173,10 +173,14 @@ func AddTempEntry(db *sql.DB, ip string, ttlSeconds int, reason, apiClientIP str // DeleteTempEntry removes a temporary whitelist entry. func DeleteTempEntry(db *sql.DB, ip, apiClientIP string) error { - _, err := db.Exec(`DELETE FROM temp_whitelist WHERE ip = ?`, ip) + result, err := db.Exec(`DELETE FROM temp_whitelist WHERE ip = ?`, ip) if err != nil { return err } + if rows, _ := result.RowsAffected(); rows == 0 { + slog.Warn("tried to delete non-existent temp ip", "ip", ip) + return nil + } _, err = db.Exec(`INSERT INTO audit_log(action, ip, api_client_ip) VALUES('delete_temp', ?, ?)`, ip, apiClientIP) if err != nil { return err @@ -186,7 +190,7 @@ func DeleteTempEntry(db *sql.DB, ip, apiClientIP string) error { // IsTempWhitelisted checks whether the given IP has a valid temporary whitelist entry. func IsTempWhitelisted(db *sql.DB, ip string) (bool, error) { - var expiresAt time.Time + var expiresAt string err := db.QueryRow(`SELECT expires_at FROM temp_whitelist WHERE ip = ?`, ip).Scan(&expiresAt) if err == sql.ErrNoRows { return false, nil @@ -194,71 +198,49 @@ func IsTempWhitelisted(db *sql.DB, ip string) (bool, error) { if err != nil { return false, err } - return expiresAt.After(time.Now()), nil + expires, parseErr := time.Parse(time.RFC3339, expiresAt) + if parseErr != nil { + slog.Error("failed to parse expires_at", "ip", ip, "error", parseErr) + return false, nil + } + return expires.After(time.Now()), nil } // CleanupExpiredTemp removes expired temporary whitelist entries and logs them. func CleanupExpiredTemp(db *sql.DB) error { - _, err := db.Exec(`DELETE FROM temp_whitelist WHERE expires_at <= ? RETURNING ip`, time.Now()) + rows, err := db.Query(`SELECT ip FROM temp_whitelist WHERE expires_at <= ?`, time.Now().Format(time.RFC3339)) if err != nil { - // SQLite may not support RETURNING; fall back to the two-step approach - var expiredIPs []string - rows, qErr := db.Query(`SELECT ip FROM temp_whitelist WHERE expires_at <= ?`, time.Now()) - if qErr != nil { - return qErr - } - for rows.Next() { - var ip string - if err := rows.Scan(&ip); err == nil { - expiredIPs = append(expiredIPs, ip) - } + return err + } + + var expiredIPs []string + for rows.Next() { + var ip string + if err := rows.Scan(&ip); err == nil { + expiredIPs = append(expiredIPs, ip) } + } + if err := rows.Err(); err != nil { rows.Close() + return err + } + rows.Close() - _, err = db.Exec(`DELETE FROM temp_whitelist WHERE expires_at <= ?`, time.Now()) - if err != nil { - return err - } - - for _, ip := range expiredIPs { - _, logErr := db.Exec(`INSERT INTO audit_log(action, ip, reason) VALUES('expire_temp', ?, ?)`, ip, "expired") - if logErr != nil { - slog.Warn("failed to log expired temp ip", "ip", ip, "error", logErr) - } - slog.Info("expired temporary whitelist", "ip", ip) - } - return nil + _, err = db.Exec(`DELETE FROM temp_whitelist WHERE expires_at <= ?`, time.Now().Format(time.RFC3339)) + if err != nil { + return err } - // The RETURNING path + for _, ip := range expiredIPs { + _, logErr := db.Exec(`INSERT INTO audit_log(action, ip, reason) VALUES('expire_temp', ?, ?)`, ip, "expired") + if logErr != nil { + slog.Warn("failed to log expired temp ip", "ip", ip, "error", logErr) + } + slog.Info("expired temporary whitelist", "ip", ip) + } return nil } -// ListTempEntries returns all valid (non-expired) temporary whitelist entries. -func ListTempEntries(db *sql.DB) ([]map[string]interface{}, error) { - rows, err := db.Query(` - SELECT ip, expires_at, reason - FROM temp_whitelist - WHERE expires_at > ? - ORDER BY expires_at - `, time.Now()) - if err != nil { - return nil, err - } - defer rows.Close() - return scanTempEntries(rows) -} - -// ListPermEntries returns all permanent whitelist entries. -func ListPermEntries(db *sql.DB) ([]map[string]interface{}, error) { - rows, err := db.Query(`SELECT ip FROM perm_whitelist ORDER BY created_at`) - if err != nil { - return nil, err - } - defer rows.Close() - return scanPermEntries(rows) -} - // ListTempEntriesPaginated returns a page of valid temporary whitelist entries. func ListTempEntriesPaginated(db *sql.DB, limit, offset int) ([]map[string]interface{}, error) { rows, err := db.Query(` @@ -267,7 +249,7 @@ func ListTempEntriesPaginated(db *sql.DB, limit, offset int) ([]map[string]inter WHERE expires_at > ? ORDER BY expires_at LIMIT ? OFFSET ? - `, time.Now(), limit, offset) + `, time.Now().Format(time.RFC3339), limit, offset) if err != nil { return nil, err } @@ -286,10 +268,10 @@ func ListPermEntriesPaginated(db *sql.DB, limit, offset int) ([]map[string]inter } func scanTempEntries(rows *sql.Rows) ([]map[string]interface{}, error) { - var list []map[string]interface{} + list := make([]map[string]interface{}, 0) for rows.Next() { var ip string - var expiresAt time.Time + var expiresAt string var reason sql.NullString if err := rows.Scan(&ip, &expiresAt, &reason); err != nil { continue @@ -300,11 +282,14 @@ func scanTempEntries(rows *sql.Rows) ([]map[string]interface{}, error) { "reason": reason.String, }) } + if err := rows.Err(); err != nil { + return nil, err + } return list, nil } func scanPermEntries(rows *sql.Rows) ([]map[string]interface{}, error) { - var list []map[string]interface{} + list := make([]map[string]interface{}, 0) for rows.Next() { var ip string if err := rows.Scan(&ip); err != nil { @@ -312,6 +297,9 @@ func scanPermEntries(rows *sql.Rows) ([]map[string]interface{}, error) { } list = append(list, map[string]interface{}{"ip": ip}) } + if err := rows.Err(); err != nil { + return nil, err + } return list, nil }