Skip to content

Commit f097d59

Browse files
authored
feat: Implement Proxy Groups for Better Organization
feat: Implement Proxy Groups for Better Organization
2 parents 8261406 + 2262639 commit f097d59

72 files changed

Lines changed: 5444 additions & 947 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/cerberus-integration.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,17 @@ jobs:
4444
scripts/cerberus_integration.sh 2>&1 | tee cerberus-test-output.txt
4545
exit "${PIPESTATUS[0]}"
4646
47+
- name: Upload Container Logs on Failure
48+
if: failure()
49+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
50+
with:
51+
name: cerberus-container-logs-${{ github.run_id }}
52+
path: |
53+
/tmp/charon-cerberus-test.log
54+
/tmp/cerberus-backend.log
55+
if-no-files-found: ignore
56+
retention-days: 7
57+
4758
- name: Dump Debug Info on Failure
4859
if: failure()
4960
run: |

.github/workflows/nightly-build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ jobs:
226226
ALPINE_IMAGE=${{ steps.alpine.outputs.image }}
227227
cache-from: type=gha
228228
cache-to: type=gha,mode=max
229+
no-cache-filter: caddy-builder
229230
provenance: true
230231
sbom: true
231232

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9090

9191
### Fixed
9292

93+
- **Proxy Groups: Dead Code Removal**: Removed unreachable code block in `resolveProxyGroupReference` — proxy host handler now has no dead branches
94+
- **Proxy Groups: Test Coverage**: Added backend tests for proxy host create/update with valid group UUID, bulk update service error, and bulk update Caddy apply error; added frontend tests for `bulkUpdateGroup` API, `useProxyHosts` bulk mutation hook, `GroupDropZone`, `ProxyHostDragHandle`, and `DataTable` renderDragHandle prop — raises PR #1018 patch coverage from 84% to ≥90%
95+
9396
- **CI: Package Deduplication**: Removed 3 duplicate `devDependency` keys in `package.json` for `@typescript-eslint/eslint-plugin`, `@typescript-eslint/parser`, and related packages — duplicate keys caused the last value to silently overwrite earlier entries
9497
- **Frontend: Fast-Refresh Violation**: Extracted `isInUse` and `isDeletable` helper functions from `CertificateList` into a new `certificateUtils` utility module to satisfy React Fast Refresh constraints (non-component exports must not share a file with components)
9598
- **Accessibility: Form Labels**: Replaced 5 invalid `<label>` elements wrapping non-labelable content with `<span>` elements in `AccessListForm`, `AccessListSelector`, and `CSPBuilder` — resolves WCAG 1.3.1 failures reported by axe-core

SECURITY.md

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ Charon users is negligible since the vulnerable code path is not exercised.
203203
|--------------|-------|
204204
| **ID** | CVE-2026-45135 |
205205
| **Severity** | High · 8.1 |
206-
| **Status** | Fix in Dockerfile (v2.11.3) — Pending Image Rebuild |
206+
| **Status** | Fix deployed — `no-cache-filter: caddy-builder` added to nightly workflow |
207207

208208
**What**
209209
Caddy v2.11.2 contains unsafe Unicode handling in the FastCGI `splitPos` function. A
@@ -225,17 +225,21 @@ potential confidentiality and integrity impact through malformed Unicode in Fast
225225

226226
- Discovered: 2026-05-20
227227
- Disclosed (if public): Public
228-
- Target fix: v2.11.3 (available)
228+
- Fixed: 2026-05-21
229229

230230
**How**
231-
Caddy is not a Go module dependency — it is built from source via xcaddy in the Dockerfile
232-
multi-stage build. The `CADDY_VERSION` ARG in the Dockerfile **is already pinned to 2.11.3**,
233-
which contains the fix. The vulnerability only affects the stale `charon:local` image built
234-
before this Dockerfile change. A container image rebuild applies the fix.
235-
236-
**Planned Remediation**
237-
Rebuild the container image. No code changes required. The fix is already present in the
238-
Dockerfile (`ARG CADDY_VERSION=2.11.3`).
231+
Caddy is built from source via xcaddy in the Dockerfile multi-stage `caddy-builder` stage.
232+
The `CADDY_VERSION` ARG was updated to `2.11.3` (commit `d94519d1`), but the nightly CI build
233+
continued to produce images containing v2.11.2. Root cause: the GHA BuildKit layer cache
234+
(`cache-from: type=gha,mode=max`) was serving the stale `caddy-builder` stage output from a
235+
prior nightly run despite the ARG value change — a known edge case where GHA cache import
236+
loses ARG-scoped metadata, preventing proper cache key invalidation.
237+
238+
**Remediation Applied**
239+
Added `no-cache-filter: caddy-builder` to the `build-and-push-nightly` job in
240+
`.github/workflows/nightly-build.yml`. This forces the `caddy-builder` stage to rebuild from
241+
scratch on every nightly run, bypassing the GHA layer cache for that stage. All other stages
242+
continue to benefit from the cache.
239243

240244
---
241245

agent/go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ require (
1111

1212
require (
1313
github.com/davecgh/go-spew v1.1.1 // indirect
14+
github.com/kr/pretty v0.3.1 // indirect
1415
github.com/pmezard/go-difflib v1.0.0 // indirect
16+
github.com/rogpeppe/go-internal v1.14.1 // indirect
1517
golang.org/x/sys v0.44.0 // indirect
18+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
1619
gopkg.in/yaml.v3 v3.0.1 // indirect
1720
)

agent/go.sum

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
1+
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
12
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
23
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
34
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
45
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
56
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
67
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
8+
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
9+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
10+
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
11+
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
12+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
13+
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
714
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
815
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
16+
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
17+
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
918
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
1019
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
1120
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
1221
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
1322
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
1423
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
15-
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
1624
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
25+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
1726
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
1827
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

backend/cmd/api/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ func main() {
118118
logger.Log().Info("Running database migrations for all models...")
119119
if err := db.AutoMigrate(
120120
// Core models
121+
&models.ProxyGroup{},
121122
&models.ProxyHost{},
122123
&models.Location{},
123124
&models.CaddyConfig{},
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package handlers
2+
3+
import (
4+
"errors"
5+
"net/http"
6+
"strings"
7+
8+
"github.com/gin-gonic/gin"
9+
"gorm.io/gorm"
10+
11+
"github.com/Wikid82/charon/backend/internal/models"
12+
"github.com/Wikid82/charon/backend/internal/services"
13+
)
14+
15+
// ProxyGroupHandler handles CRUD operations for proxy groups.
16+
type ProxyGroupHandler struct {
17+
service *services.ProxyGroupService
18+
db *gorm.DB
19+
}
20+
21+
// NewProxyGroupHandler creates a new ProxyGroupHandler.
22+
func NewProxyGroupHandler(db *gorm.DB) *ProxyGroupHandler {
23+
return &ProxyGroupHandler{
24+
service: services.NewProxyGroupService(db),
25+
db: db,
26+
}
27+
}
28+
29+
// RegisterRoutes registers proxy group routes on the given router group.
30+
func (h *ProxyGroupHandler) RegisterRoutes(router *gin.RouterGroup) {
31+
router.GET("/proxy-groups", h.List)
32+
router.POST("/proxy-groups", h.Create)
33+
router.GET("/proxy-groups/:uuid", h.Get)
34+
router.PUT("/proxy-groups/:uuid", h.Update)
35+
router.DELETE("/proxy-groups/:uuid", h.Delete)
36+
}
37+
38+
// List returns all proxy groups ordered by name.
39+
func (h *ProxyGroupHandler) List(c *gin.Context) {
40+
groups, err := h.service.List()
41+
if err != nil {
42+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
43+
return
44+
}
45+
c.JSON(http.StatusOK, groups)
46+
}
47+
48+
// Create creates a new proxy group.
49+
func (h *ProxyGroupHandler) Create(c *gin.Context) {
50+
var payload struct {
51+
Name string `json:"name"`
52+
Description string `json:"description"`
53+
Color string `json:"color"`
54+
}
55+
if err := c.ShouldBindJSON(&payload); err != nil {
56+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
57+
return
58+
}
59+
60+
group := models.ProxyGroup{
61+
Name: strings.TrimSpace(payload.Name),
62+
Description: payload.Description,
63+
Color: payload.Color,
64+
}
65+
if group.Color == "" {
66+
group.Color = "#6366f1"
67+
}
68+
69+
if err := h.service.Create(&group); err != nil {
70+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
71+
return
72+
}
73+
c.JSON(http.StatusCreated, group)
74+
}
75+
76+
// Get returns a single proxy group by UUID, including host count.
77+
func (h *ProxyGroupHandler) Get(c *gin.Context) {
78+
group, err := h.service.GetByUUID(c.Param("uuid"))
79+
if err != nil {
80+
if errors.Is(err, services.ErrProxyGroupNotFound) {
81+
c.JSON(http.StatusNotFound, gin.H{"error": "proxy group not found"})
82+
} else {
83+
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve proxy group"})
84+
}
85+
return
86+
}
87+
count, err := h.service.GetHostCount(group.ID)
88+
if err != nil {
89+
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve host count"})
90+
return
91+
}
92+
c.JSON(http.StatusOK, gin.H{
93+
"uuid": group.UUID,
94+
"name": group.Name,
95+
"description": group.Description,
96+
"color": group.Color,
97+
"host_count": count,
98+
"created_at": group.CreatedAt,
99+
"updated_at": group.UpdatedAt,
100+
})
101+
}
102+
103+
// Update updates an existing proxy group by UUID.
104+
func (h *ProxyGroupHandler) Update(c *gin.Context) {
105+
group, err := h.service.GetByUUID(c.Param("uuid"))
106+
if err != nil {
107+
if errors.Is(err, services.ErrProxyGroupNotFound) {
108+
c.JSON(http.StatusNotFound, gin.H{"error": "proxy group not found"})
109+
} else {
110+
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve proxy group"})
111+
}
112+
return
113+
}
114+
115+
var payload struct {
116+
Name *string `json:"name"`
117+
Description *string `json:"description"`
118+
Color *string `json:"color"`
119+
}
120+
if err := c.ShouldBindJSON(&payload); err != nil {
121+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
122+
return
123+
}
124+
125+
if payload.Name != nil {
126+
trimmed := strings.TrimSpace(*payload.Name)
127+
if trimmed == "" {
128+
c.JSON(http.StatusBadRequest, gin.H{"error": "name cannot be empty"})
129+
return
130+
}
131+
group.Name = trimmed
132+
}
133+
if payload.Description != nil {
134+
group.Description = *payload.Description
135+
}
136+
if payload.Color != nil {
137+
group.Color = *payload.Color
138+
}
139+
140+
if err := h.service.Update(group); err != nil {
141+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
142+
return
143+
}
144+
c.JSON(http.StatusOK, group)
145+
}
146+
147+
// Delete removes a proxy group by UUID, clearing host assignments.
148+
func (h *ProxyGroupHandler) Delete(c *gin.Context) {
149+
group, err := h.service.GetByUUID(c.Param("uuid"))
150+
if err != nil {
151+
if errors.Is(err, services.ErrProxyGroupNotFound) {
152+
c.JSON(http.StatusNotFound, gin.H{"error": "proxy group not found"})
153+
} else {
154+
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve proxy group"})
155+
}
156+
return
157+
}
158+
if err := h.service.Delete(group.ID); err != nil {
159+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
160+
return
161+
}
162+
c.Status(http.StatusNoContent)
163+
}

0 commit comments

Comments
 (0)