You've already forked openaccounting-server
forked from cybercinch/openaccounting-server
feat: implement secure file upload system with JWT authentication
- Add JWT-based secure file access for local storage with 1-hour expiry - Implement GORM repository methods for attachment CRUD operations - Add secure file serving endpoint with token validation - Update storage interface to support user context in URL generation - Add comprehensive security features including path traversal protection - Update documentation with security model and configuration examples - Add utility functions for hex/byte conversion and UUID validation - Configure secure file permissions (0600) for uploaded files 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ package storage
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -13,8 +14,9 @@ import (
|
||||
|
||||
// LocalStorage implements the Storage interface for local filesystem
|
||||
type LocalStorage struct {
|
||||
rootDir string
|
||||
baseURL string
|
||||
rootDir string
|
||||
baseURL string
|
||||
tokenService *TokenService
|
||||
}
|
||||
|
||||
// NewLocalStorage creates a new local filesystem storage backend
|
||||
@@ -24,14 +26,18 @@ func NewLocalStorage(config LocalConfig) (*LocalStorage, error) {
|
||||
rootDir = "./uploads"
|
||||
}
|
||||
|
||||
// Ensure the root directory exists
|
||||
if err := os.MkdirAll(rootDir, 0755); err != nil {
|
||||
// Ensure the root directory exists with secure permissions
|
||||
if err := os.MkdirAll(rootDir, 0700); err != nil {
|
||||
return nil, fmt.Errorf("failed to create storage directory: %w", err)
|
||||
}
|
||||
|
||||
// Initialize token service for secure URL generation
|
||||
tokenService := NewTokenService(config.SigningKey)
|
||||
|
||||
return &LocalStorage{
|
||||
rootDir: rootDir,
|
||||
baseURL: config.BaseURL,
|
||||
rootDir: rootDir,
|
||||
baseURL: config.BaseURL,
|
||||
tokenService: tokenService,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -41,14 +47,14 @@ func (l *LocalStorage) Store(filename string, content io.Reader, contentType str
|
||||
storagePath := l.generateStoragePath(filename)
|
||||
fullPath := filepath.Join(l.rootDir, storagePath)
|
||||
|
||||
// Ensure the directory exists
|
||||
// Ensure the directory exists with secure permissions
|
||||
dir := filepath.Dir(fullPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
return "", fmt.Errorf("failed to create directory: %w", err)
|
||||
}
|
||||
|
||||
// Create and write the file
|
||||
file, err := os.Create(fullPath)
|
||||
// Create and write the file with secure permissions
|
||||
file, err := os.OpenFile(fullPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create file: %w", err)
|
||||
}
|
||||
@@ -102,8 +108,13 @@ func (l *LocalStorage) Delete(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetURL returns a URL for accessing the file
|
||||
// GetURL returns a secure URL for accessing the file with JWT token
|
||||
func (l *LocalStorage) GetURL(path string, expiry time.Duration) (string, error) {
|
||||
return l.GetURLWithContext(path, expiry, "", "")
|
||||
}
|
||||
|
||||
// GetURLWithContext returns a secure URL for accessing the file with JWT token and user context
|
||||
func (l *LocalStorage) GetURLWithContext(path string, expiry time.Duration, userID, orgID string) (string, error) {
|
||||
// Validate path to prevent directory traversal
|
||||
if err := l.validatePath(path); err != nil {
|
||||
return "", err
|
||||
@@ -118,14 +129,16 @@ func (l *LocalStorage) GetURL(path string, expiry time.Duration) (string, error)
|
||||
return "", &FileNotFoundError{Path: path}
|
||||
}
|
||||
|
||||
if l.baseURL != "" {
|
||||
// Return a public URL if base URL is configured
|
||||
return l.baseURL + "/" + path, nil
|
||||
// Generate secure JWT token for file access
|
||||
token, err := l.tokenService.GenerateFileToken(path, userID, orgID, expiry)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate access token: %w", err)
|
||||
}
|
||||
|
||||
// For local storage without a base URL, return the file path
|
||||
// In a real application, you might serve these through an endpoint
|
||||
return "/files/" + path, nil
|
||||
// Return secure URL with token parameter
|
||||
params := url.Values{}
|
||||
params.Set("token", token)
|
||||
return "/secure-files?" + params.Encode(), nil
|
||||
}
|
||||
|
||||
// Exists checks if a file exists at the given path
|
||||
|
||||
Reference in New Issue
Block a user