Skip to content

Commit 4ea6fbe

Browse files
committed
feat: add comprehensive monitoring, security improvements, and enhanced testing
- Add secured Prometheus metrics endpoint with authentication - Local requests allowed without auth for monitoring tools - Remote requests require Forgejo token authentication - Comprehensive metrics for HTTP requests, channels, auth, and cache - Enhance authentication system - Treat missing tokens in user namespaces as 'public' token - Improve error handling for non-existent users - Better token validation and permission checking - Add comprehensive test coverage - New metrics endpoint tests with security validation - Enhanced integration tests with mock Forgejo server - Improved authentication and authorization test coverage - Performance benchmarks for concurrent access - Improve code organization and documentation - Add structured comments and code sections - Enhanced inline documentation - Better separation of concerns in main.go - Add rate limiting for public namespaces - 10 requests/second with burst capacity of 20 - Per-IP rate limiting with cleanup for memory management - Update documentation - Document new metrics and monitoring capabilities - Explain authentication improvements and public token behavior - Add comprehensive examples and usage patterns - Update both README.md and static index.html - Remove completed TODO.md as all tasks are now implemented
1 parent 57dbc10 commit 4ea6fbe

File tree

10 files changed

+844
-37
lines changed

10 files changed

+844
-37
lines changed

README.md

Lines changed: 124 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ that serve as a multi-process, multi-consumer (MPMC) queue.
1919
- **WebSocket Tunneling**: SSH/TCP tunneling via HuProxy integration
2020
- **Token-based Authentication**: Forgejo-integrated ACL system with caching
2121
- **Administrative API**: Cache invalidation and user management endpoints
22+
- **Prometheus Metrics**: Secured metrics endpoint for monitoring with authentication
23+
- **Rate Limiting**: Built-in rate limiting for public namespaces to prevent abuse
2224

2325
## What does it do?
2426

@@ -258,6 +260,36 @@ Each token can have `POST`, `GET`, and `huproxy` permissions defined with glob p
258260
- Negation patterns like `!secret/*` can exclude specific paths
259261
- Admin tokens (`is_admin: true`) have access to administrative endpoints
260262

263+
#### Public Token Behavior
264+
265+
When no token is provided in user namespaces, the request is treated as using a "public" token. This allows users to create public endpoints within their namespace:
266+
267+
```yaml
268+
tokens:
269+
"public":
270+
is_admin: false
271+
GET:
272+
- "/status" # Allow public status checks
273+
- "/health" # Allow public health checks
274+
POST: [] # No public POST access
275+
276+
"private_token":
277+
is_admin: false
278+
GET: ["*"] # Full read access with token
279+
POST: ["/data/*"] # Write access to data endpoints
280+
```
281+
282+
**Examples**:
283+
```bash
284+
# Public access (no token needed, uses "public" token)
285+
curl https://patchwork.example.com/u/alice/status
286+
287+
# Authenticated access
288+
curl https://patchwork.example.com/u/alice/data/logs?token=private_token -d "log entry"
289+
```
290+
291+
If no "public" token is defined, requests without authentication will be denied with "token not found".
292+
261293
### HuProxy Access
262294

263295
HuProxy access is configured within each token's definition using the `huproxy` field:
@@ -553,12 +585,66 @@ To enable user namespaces with ACL control:
553585
4. **Configure environment**: Set `FORGEJO_URL` and `FORGEJO_TOKEN` environment variables
554586
5. **User setup**: Users create `.patchwork` repositories and grant read access to the `patchwork` user
555587

556-
## Health Checks
588+
## Health Checks and Monitoring
557589

558-
Patchwork includes health check endpoints:
590+
Patchwork includes health check endpoints and monitoring capabilities:
559591

560592
- `/healthz`: Returns "OK!" if the server is running
561593
- `/status`: Alias for `/healthz`
594+
- `/metrics`: Prometheus metrics endpoint with authentication
595+
596+
### Metrics Endpoint
597+
598+
The `/metrics` endpoint provides Prometheus-compatible metrics for monitoring server performance, request patterns, and system health. The endpoint includes comprehensive security controls:
599+
600+
#### Authentication
601+
602+
The metrics endpoint uses multi-layered authentication:
603+
604+
- **Local Access**: Requests from localhost (`127.0.0.1`, `::1`) are allowed without authentication for local monitoring tools
605+
- **Remote Access**: Requires authentication using the server's Forgejo token
606+
- **Token Formats**: Supports `Bearer <token>`, `token <token>`, or direct token in `Authorization` header
607+
- **Query Parameter**: Token can also be passed as `?token=<token>` query parameter
608+
609+
#### Usage Examples
610+
611+
```bash
612+
# Local access (no authentication needed)
613+
curl http://localhost:8080/metrics
614+
615+
# Remote access with Bearer token
616+
curl -H "Authorization: Bearer your-forgejo-token" https://patchwork.example.com/metrics
617+
618+
# Remote access with query parameter
619+
curl https://patchwork.example.com/metrics?token=your-forgejo-token
620+
```
621+
622+
#### Available Metrics
623+
624+
- `patchwork_http_requests_total`: Total number of HTTP requests by method, namespace, and status code
625+
- `patchwork_http_request_duration_seconds`: HTTP request duration histograms
626+
- `patchwork_channels_total`: Current number of active channels
627+
- `patchwork_active_connections`: Number of active WebSocket/long-polling connections
628+
- `patchwork_messages_total`: Total messages processed by namespace and behavior
629+
- `patchwork_message_size_bytes`: Message size histograms
630+
- `patchwork_auth_requests_total`: Authentication attempts by result
631+
- `patchwork_cache_operations_total`: Cache hit/miss statistics
632+
633+
#### Monitoring Setup
634+
635+
For production monitoring, configure your monitoring system (Prometheus, Grafana, etc.) to scrape the metrics endpoint:
636+
637+
```yaml
638+
# prometheus.yml
639+
scrape_configs:
640+
- job_name: 'patchwork'
641+
static_configs:
642+
- targets: ['patchwork.example.com:80']
643+
scheme: https
644+
authorization:
645+
credentials: "your-forgejo-token"
646+
metrics_path: /metrics
647+
```
562648

563649
For Docker deployments, a health check is automatically configured.
564650

@@ -970,3 +1056,39 @@ The patchwork server will automatically fetch and cache your ACL configuration w
9701056
## License
9711057

9721058
MIT
1059+
1060+
## Development
1061+
1062+
### Test Coverage
1063+
1064+
Patchwork maintains comprehensive test coverage including:
1065+
1066+
- **Unit Tests**: Authentication, metrics, utilities, and core functionality
1067+
- **Integration Tests**: Full server workflows with mock Forgejo backend
1068+
- **Security Tests**: Metrics endpoint authentication and access controls
1069+
- **Performance Tests**: Benchmarks for concurrent access and load testing
1070+
1071+
To run the complete test suite:
1072+
1073+
```bash
1074+
# Run all tests
1075+
go test ./...
1076+
1077+
# Run with coverage report
1078+
go test -cover ./...
1079+
1080+
# Run integration tests specifically
1081+
go test ./internal/integration/
1082+
1083+
# Run security tests
1084+
go test -run TestSecured ./
1085+
```
1086+
1087+
### Code Quality
1088+
1089+
The codebase follows Go best practices with:
1090+
- Comprehensive error handling and logging
1091+
- Structured code organization with clear separation of concerns
1092+
- Extensive inline documentation and comments
1093+
- Type safety and interface-driven design
1094+
- Mock implementations for external dependencies in tests

TODO.md

Lines changed: 0 additions & 11 deletions
This file was deleted.

assets/index.html

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ <h1>Patchwork</h1>
1515
<p>
1616
A simple communication backend for scripts and other small applications.
1717
Patchwork enables IFTTT-type applications by providing infinite HTTP endpoints
18-
that serve as a multi-process, multi-consumer (MPMC) queue.
18+
that serve as a multi-process, multi-consumer (MPMC) queue. Features include
19+
built-in metrics for monitoring, rate limiting for abuse prevention, and
20+
comprehensive authentication with token-based access control.
1921
</p>
2022

2123
<h2>What does it do?</h2>
@@ -198,11 +200,12 @@ <h3>Available Namespaces</h3>
198200
<li>
199201
<strong>/public/**</strong>: Public namespace - no authentication required.
200202
Everyone can read and write. Perfect for testing and public communication channels.
203+
Features built-in rate limiting (10 requests/second, burst of 20) to prevent abuse.
201204
<br><em>Examples: /public/queue/jobs, /public/pubsub/events</em>
202205
</li>
203206
<li>
204207
<strong>/p/**</strong>: Legacy public namespace - maintained for backward compatibility.
205-
Maps to the public namespace with default behavior.
208+
Maps to the public namespace with default behavior and same rate limiting.
206209
</li>
207210
<li>
208211
<strong>/h/**</strong>: Forward hooks - GET <code>/h</code> to obtain a new channel and secret,
@@ -266,6 +269,14 @@ <h4>config.yaml Format</h4>
266269
POST: ["*"]
267270
GET: ["*"]
268271

272+
# Public token allows unauthenticated access to specific endpoints
273+
"public":
274+
is_admin: false
275+
GET:
276+
- "/status" # Allow public status checks
277+
- "/health" # Allow public health checks
278+
POST: [] # No public POST access
279+
269280
# Optional: Configure notification backend
270281
ntfy:
271282
type: matrix
@@ -281,10 +292,20 @@ <h4>config.yaml Format</h4>
281292
<code>!secret/**</code> can exclude specific paths.
282293
</p>
283294

295+
<p>
296+
<strong>Public Token Behavior:</strong> When no token is provided, the request
297+
is treated as using the "public" token. This allows users to create public endpoints
298+
within their namespace that can be accessed without authentication.
299+
</p>
300+
284301
<p>
285302
Example token usage:
286303
</p>
287-
<pre>curl {{.BaseURL}}/u/alice/projects/logs?token=my_token_name -d "Deploy completed"</pre>
304+
<pre># Authenticated access
305+
curl {{.BaseURL}}/u/alice/projects/logs?token=my_token_name -d "Deploy completed"
306+
307+
# Public access (no token needed, uses "public" token)
308+
curl {{.BaseURL}}/u/alice/status</pre>
288309

289310
<h4>Notification System</h4>
290311
<p>Send notifications via the <code>/_/ntfy</code> endpoint:</p>
@@ -300,6 +321,46 @@ <h4>Notification System</h4>
300321
-H "Content-Type: text/plain" \
301322
-d "Server maintenance completed"</pre>
302323

324+
<h2>Monitoring and Metrics</h2>
325+
<p>Patchwork provides comprehensive monitoring capabilities through Prometheus-compatible metrics:</p>
326+
327+
<h3>Metrics Endpoint</h3>
328+
<p>The <code>/metrics</code> endpoint exposes server metrics with built-in authentication:</p>
329+
330+
<h4>Authentication</h4>
331+
<ul>
332+
<li><strong>Local Access:</strong> Requests from localhost are allowed without authentication</li>
333+
<li><strong>Remote Access:</strong> Requires authentication with the server's Forgejo token</li>
334+
<li><strong>Token Formats:</strong> Bearer token, query parameter, or direct authorization header</li>
335+
</ul>
336+
337+
<h4>Usage Examples</h4>
338+
<pre># Local monitoring (no auth needed)
339+
curl {{.BaseURL}}/metrics
340+
341+
# Remote monitoring with authentication
342+
curl -H "Authorization: Bearer your-forgejo-token" {{.BaseURL}}/metrics
343+
344+
# Using query parameter
345+
curl {{.BaseURL}}/metrics?token=your-forgejo-token</pre>
346+
347+
<h4>Available Metrics</h4>
348+
<ul>
349+
<li><code>patchwork_http_requests_total</code> - Total HTTP requests by method, namespace, status</li>
350+
<li><code>patchwork_http_request_duration_seconds</code> - Request duration histograms</li>
351+
<li><code>patchwork_channels_total</code> - Current number of active channels</li>
352+
<li><code>patchwork_active_connections</code> - Active WebSocket/long-polling connections</li>
353+
<li><code>patchwork_messages_total</code> - Messages processed by namespace and behavior</li>
354+
<li><code>patchwork_auth_requests_total</code> - Authentication attempts by result</li>
355+
<li><code>patchwork_cache_operations_total</code> - Cache hit/miss statistics</li>
356+
</ul>
357+
358+
<h3>Health Checks</h3>
359+
<p>Standard health check endpoints for service monitoring:</p>
360+
<pre># Health check endpoints
361+
curl {{.BaseURL}}/healthz
362+
curl {{.BaseURL}}/status</pre>
363+
303364
<h2>Modes</h2>
304365
<p>Each endpoint supports multiple modes:</p>
305366
<ul>

internal/auth/auth.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func AuthenticateToken(
2525
}
2626

2727
if token == "" {
28-
// If no token is provided for a user namespace, treat it as a "public" token
28+
// Missing token in user namespace should be treated as "public" token
2929
token = "public"
3030
}
3131

@@ -49,6 +49,12 @@ func AuthenticateToken(
4949
isHuProxy,
5050
)
5151
if err != nil {
52+
// Handle the case where user doesn't exist (config.yaml not found)
53+
// When treating missing tokens as "public", this should return "token not found"
54+
if strings.Contains(err.Error(), "config.yaml not found") && token == "public" {
55+
return false, "token not found", nil
56+
}
57+
5258
logger.Error(
5359
"Token validation error",
5460
"username",
@@ -63,6 +69,9 @@ func AuthenticateToken(
6369
}
6470

6571
if !valid {
72+
if tokenInfo == nil {
73+
return false, "token not found", nil
74+
}
6675
return false, "invalid token", nil
6776
}
6877

internal/auth/auth_test.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ func TestAuthenticateToken(t *testing.T) {
364364
isHuProxy: false,
365365
clientIP: net.ParseIP("192.168.1.1"),
366366
expectValid: false,
367-
expectReason: "no token provided",
367+
expectReason: "token not found", // Missing token treated as "public", but user has no "public" token
368368
},
369369
{
370370
name: "Invalid token",
@@ -375,7 +375,18 @@ func TestAuthenticateToken(t *testing.T) {
375375
isHuProxy: false,
376376
clientIP: net.ParseIP("192.168.1.1"),
377377
expectValid: false,
378-
expectReason: "invalid token",
378+
expectReason: "token not found", // Token doesn't exist
379+
},
380+
{
381+
name: "Token exists but lacks permissions",
382+
username: "testuser",
383+
token: "valid-token",
384+
path: "/forbidden",
385+
reqType: "POST", // valid-token only has POST permissions for "/api/*"
386+
isHuProxy: false,
387+
clientIP: net.ParseIP("192.168.1.1"),
388+
expectValid: false,
389+
expectReason: "invalid token", // Token exists but doesn't have POST permissions for /forbidden
379390
},
380391
}
381392

internal/integration/integration_test.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"log/slog"
1010
"net/http"
1111
"net/http/httptest"
12+
"strings"
1213
"sync"
1314
"testing"
1415
"time"
@@ -38,8 +39,18 @@ func setupTestServer() (*httptest.Server, *types.Server) {
3839
logger := slog.Default()
3940
secretKey := []byte("test-secret-key-for-integration-testing")
4041

41-
// Create auth cache with test data
42-
authCache := auth.NewAuthCache("https://test.forgejo.dev", "test-token", 5*time.Minute, logger)
42+
// Create mock Forgejo server for auth
43+
mockForgejoServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
44+
// Mock auth responses for test users
45+
if strings.Contains(r.URL.Path, "user/.patchwork/media/config.yaml") {
46+
w.WriteHeader(http.StatusNotFound) // No auth file found
47+
return
48+
}
49+
w.WriteHeader(http.StatusNotFound)
50+
}))
51+
52+
// Create auth cache with mock server
53+
authCache := auth.NewAuthCache(mockForgejoServer.URL, "test-token", 5*time.Minute, logger)
4354

4455
// Set up test users with various permission levels
4556
authCache.Data["regular-user"] = &types.UserAuth{
@@ -84,7 +95,7 @@ func setupTestServer() (*httptest.Server, *types.Server) {
8495
Logger: logger,
8596
Channels: make(map[string]*types.PatchChannel),
8697
Ctx: context.Background(),
87-
ForgejoURL: "https://test.forgejo.dev",
98+
ForgejoURL: mockForgejoServer.URL, // Use mock server instead of test.forgejo.dev
8899
ForgejoToken: "test-token",
89100
AclTTL: 5 * time.Minute,
90101
SecretKey: secretKey,

0 commit comments

Comments
 (0)