Skip to content

Commit 56b131b

Browse files
authored
fix: events create rbac role removed cause its pointless (#2795)
1 parent a615696 commit 56b131b

24 files changed

Lines changed: 527 additions & 269 deletions

backend/api/api.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ func registerHandlersInternal(api huma.API, svc *di.Services, handlerAppCtx hand
285285
handlers.RegisterUsers(api, svc.User, svc.Auth)
286286
handlers.RegisterProjects(api, svc.Project, svc.Activity, handlerAppCtx)
287287
handlers.RegisterVersion(api, svc.Version)
288-
handlers.RegisterEvents(api, svc.Event, svc.ApiKey)
288+
handlers.RegisterEvents(api, svc.Event)
289289
handlers.RegisterActivities(api, svc.Activity, svc.Environment)
290290
handlers.RegisterOidc(api, svc.Auth, svc.Oidc, svc.Role, svc.User, cfg)
291291
handlers.RegisterEnvironments(api, svc.Environment, svc.Settings, svc.ApiKey, svc.Event, cfg)

backend/api/api_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,15 @@ func TestSetupAPIForSpec_TemplateReadRoutesProtected(t *testing.T) {
168168
})
169169
}
170170
}
171+
172+
func TestSetupAPIForSpec_DoesNotRegisterPublicCreateEvent(t *testing.T) {
173+
api := SetupAPIForSpec()
174+
175+
pathItem := api.OpenAPI().Paths["/events"]
176+
if pathItem == nil {
177+
t.Fatal("expected /events path to be registered for list events")
178+
}
179+
if pathItem.Post != nil {
180+
t.Fatal("expected POST /events to be absent from the public API")
181+
}
182+
}

backend/api/handlers/environments.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ type DeploymentSnippet struct {
156156
type DeploymentSnippetFile struct {
157157
Name string `json:"name" doc:"Suggested filename"`
158158
Content string `json:"content,omitempty" doc:"PEM file contents. Omitted for sensitive files such as private keys; use downloadUrl instead."`
159-
DownloadURL string `json:"downloadUrl,omitempty" doc:"Admin-only endpoint to download this file when content is withheld"`
159+
DownloadURL string `json:"downloadUrl,omitempty" doc:"Pairing-permission endpoint to download this file when content is withheld"`
160160
Sensitive bool `json:"sensitive,omitempty" doc:"True when this file is sensitive and must be fetched via downloadUrl"`
161161
ContainerPath string `json:"containerPath" doc:"Container mount path expected by the mTLS snippet"`
162162
Permissions string `json:"permissions" doc:"Suggested file mode"`
@@ -358,7 +358,7 @@ func RegisterEnvironments(api huma.API, environmentService *services.Environment
358358
{"BearerAuth": {}},
359359
{"ApiKeyAuth": {}},
360360
},
361-
Middlewares: humamw.RequirePermission(api, authz.PermEnvironmentsRead),
361+
Middlewares: humamw.RequirePermission(api, authz.PermEnvironmentsPair),
362362
}, h.GetDeploymentSnippets)
363363

364364
huma.Register(api, huma.Operation{
@@ -372,7 +372,7 @@ func RegisterEnvironments(api huma.API, environmentService *services.Environment
372372
{"BearerAuth": {}},
373373
{"ApiKeyAuth": {}},
374374
},
375-
Middlewares: humamw.RequirePermission(api, authz.PermEnvironmentsRead),
375+
Middlewares: humamw.RequirePermission(api, authz.PermEnvironmentsPair),
376376
}, h.DownloadEnvironmentMTLSBundle)
377377

378378
huma.Register(api, huma.Operation{
@@ -386,7 +386,7 @@ func RegisterEnvironments(api huma.API, environmentService *services.Environment
386386
{"BearerAuth": {}},
387387
{"ApiKeyAuth": {}},
388388
},
389-
Middlewares: humamw.RequirePermission(api, authz.PermEnvironmentsRead),
389+
Middlewares: humamw.RequirePermission(api, authz.PermEnvironmentsPair),
390390
}, h.DownloadEnvironmentMTLSFile)
391391

392392
huma.Register(api, huma.Operation{
@@ -414,7 +414,7 @@ func RegisterEnvironments(api huma.API, environmentService *services.Environment
414414
{"BearerAuth": {}},
415415
{"ApiKeyAuth": {}},
416416
},
417-
Middlewares: humamw.RequirePermission(api, authz.PermEnvironmentsRead),
417+
Middlewares: humamw.RequirePermission(api, authz.PermEnvironmentsPair),
418418
}, h.DownloadEdgeMTLSCA)
419419
}
420420

backend/api/handlers/environments_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,62 @@
11
package handlers
22

33
import (
4+
"net/http"
5+
"net/http/httptest"
46
"os"
57
"testing"
68
"time"
79

810
"github.com/getarcaneapp/arcane/backend/internal/models"
911
"github.com/getarcaneapp/arcane/backend/internal/services"
12+
"github.com/getarcaneapp/arcane/backend/pkg/authz"
1013
"github.com/getarcaneapp/arcane/backend/pkg/libarcane/edge"
1114
envtypes "github.com/getarcaneapp/arcane/types/environment"
1215
"github.com/stretchr/testify/assert"
16+
"github.com/stretchr/testify/require"
1317
)
1418

19+
func TestEnvironmentSecretDeploymentRoutesRequirePairPermission(t *testing.T) {
20+
testCases := []struct {
21+
name string
22+
path string
23+
}{
24+
{name: "deployment snippets", path: "/api/environments/env-1/deployment"},
25+
{name: "mtls bundle", path: "/api/environments/env-1/deployment/mtls/bundle"},
26+
{name: "mtls file", path: "/api/environments/env-1/deployment/mtls/agent.key"},
27+
{name: "edge ca", path: "/api/edge-mtls/ca"},
28+
}
29+
30+
for _, testCase := range testCases {
31+
t.Run(testCase.name+" read denied", func(t *testing.T) {
32+
ps := authz.NewPermissionSet()
33+
ps.AddGlobal(authz.PermEnvironmentsRead)
34+
router, api := newPermissionGatingRouterInternal(t, ps)
35+
RegisterEnvironments(api, nil, nil, nil, nil, nil)
36+
37+
req := httptest.NewRequest(http.MethodGet, testCase.path, nil)
38+
rec := httptest.NewRecorder()
39+
router.ServeHTTP(rec, req)
40+
41+
require.Equal(t, http.StatusForbidden, rec.Code)
42+
require.Contains(t, rec.Body.String(), authz.PermEnvironmentsPair)
43+
})
44+
45+
t.Run(testCase.name+" pair allowed", func(t *testing.T) {
46+
ps := authz.NewPermissionSet()
47+
ps.AddGlobal(authz.PermEnvironmentsPair)
48+
router, api := newPermissionGatingRouterInternal(t, ps)
49+
RegisterEnvironments(api, nil, nil, nil, nil, nil)
50+
51+
req := httptest.NewRequest(http.MethodGet, testCase.path, nil)
52+
rec := httptest.NewRecorder()
53+
router.ServeHTTP(rec, req)
54+
55+
require.NotEqual(t, http.StatusForbidden, rec.Code)
56+
})
57+
}
58+
}
59+
1560
func TestEnvironmentMTLSAssetDownloadName(t *testing.T) {
1661
env := &models.Environment{Name: "Lab Server"}
1762
env.ID = "env-123"

backend/api/handlers/events.go

Lines changed: 71 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,26 @@ package handlers
22

33
import (
44
"context"
5+
"encoding/json"
6+
"log/slog"
7+
"net/http"
8+
"strings"
59

610
"github.com/danielgtaylor/huma/v2"
711
humamw "github.com/getarcaneapp/arcane/backend/api/middleware"
812
"github.com/getarcaneapp/arcane/backend/internal/common"
13+
"github.com/getarcaneapp/arcane/backend/internal/config"
914
"github.com/getarcaneapp/arcane/backend/internal/services"
1015
"github.com/getarcaneapp/arcane/backend/pkg/authz"
16+
pkgutils "github.com/getarcaneapp/arcane/backend/pkg/utils"
1117
"github.com/getarcaneapp/arcane/types/base"
1218
"github.com/getarcaneapp/arcane/types/event"
19+
"github.com/labstack/echo/v4"
1320
)
1421

1522
// EventHandler handles event management endpoints.
1623
type EventHandler struct {
1724
eventService *services.EventService
18-
apiKeySvc *services.ApiKeyService
1925
}
2026

2127
// ============================================================================
@@ -58,15 +64,6 @@ type GetEventsByEnvironmentOutput struct {
5864
Body EventPaginatedResponse
5965
}
6066

61-
type CreateEventInput struct {
62-
XAPIKey string `header:"X-API-Key" doc:"API key for environment-scoped event forwarding"`
63-
Body event.CreateEvent
64-
}
65-
66-
type CreateEventOutput struct {
67-
Body base.ApiResponse[event.Event]
68-
}
69-
7067
type DeleteEventInput struct {
7168
EventID string `path:"eventId" doc:"Event ID"`
7269
}
@@ -79,11 +76,72 @@ type DeleteEventOutput struct {
7976
// Registration
8077
// ============================================================================
8178

79+
// RegisterAgentEventIngestion registers the manager ingestion endpoint used by
80+
// direct agents when no edge tunnel is active. This route is not part of the
81+
// Huma/OpenAPI surface and authenticates only with the configured agent token.
82+
func RegisterAgentEventIngestion(g *echo.Group, eventService *services.EventService, cfg *config.Config) {
83+
g.POST("/events", func(c echo.Context) error {
84+
if eventService == nil {
85+
return c.JSON(http.StatusInternalServerError, base.ApiResponse[base.MessageResponse]{
86+
Success: false,
87+
Data: base.MessageResponse{Message: "service not available"},
88+
})
89+
}
90+
if cfg == nil || strings.TrimSpace(cfg.AgentToken) == "" {
91+
slog.Warn("agent event ingestion is disabled because agent token is not configured")
92+
return c.JSON(http.StatusServiceUnavailable, base.ApiResponse[base.MessageResponse]{
93+
Success: false,
94+
Data: base.MessageResponse{Message: "agent event ingestion is not configured"},
95+
})
96+
}
97+
if !validAgentEventIngestionTokenInternal(c.Request(), cfg) {
98+
return c.JSON(http.StatusUnauthorized, base.ApiResponse[base.MessageResponse]{
99+
Success: false,
100+
Data: base.MessageResponse{Message: "invalid agent token"},
101+
})
102+
}
103+
104+
var input services.CreateEventRequest
105+
decoder := json.NewDecoder(http.MaxBytesReader(c.Response(), c.Request().Body, 1<<20))
106+
if err := decoder.Decode(&input); err != nil {
107+
return c.JSON(http.StatusBadRequest, base.ApiResponse[base.MessageResponse]{
108+
Success: false,
109+
Data: base.MessageResponse{Message: "invalid event payload"},
110+
})
111+
}
112+
if strings.TrimSpace(string(input.Type)) == "" || strings.TrimSpace(input.Title) == "" {
113+
return c.JSON(http.StatusBadRequest, base.ApiResponse[base.MessageResponse]{
114+
Success: false,
115+
Data: base.MessageResponse{Message: "event type and title are required"},
116+
})
117+
}
118+
119+
if _, err := eventService.CreateEvent(c.Request().Context(), input); err != nil {
120+
return c.JSON(http.StatusInternalServerError, base.ApiResponse[base.MessageResponse]{
121+
Success: false,
122+
Data: base.MessageResponse{Message: (&common.EventCreationError{Err: err}).Error()},
123+
})
124+
}
125+
126+
return c.JSON(http.StatusAccepted, base.ApiResponse[base.MessageResponse]{
127+
Success: true,
128+
Data: base.MessageResponse{Message: "event ingested"},
129+
})
130+
})
131+
}
132+
133+
func validAgentEventIngestionTokenInternal(r *http.Request, cfg *config.Config) bool {
134+
if cfg == nil {
135+
return false
136+
}
137+
token := r.Header.Get(pkgutils.HeaderAgentToken)
138+
return token != "" && token == cfg.AgentToken
139+
}
140+
82141
// RegisterEvents registers all event management endpoints.
83-
func RegisterEvents(api huma.API, eventService *services.EventService, apiKeySvc *services.ApiKeyService) {
142+
func RegisterEvents(api huma.API, eventService *services.EventService) {
84143
h := &EventHandler{
85144
eventService: eventService,
86-
apiKeySvc: apiKeySvc,
87145
}
88146

89147
huma.Register(api, huma.Operation{
@@ -100,23 +158,6 @@ func RegisterEvents(api huma.API, eventService *services.EventService, apiKeySvc
100158
Middlewares: humamw.RequirePermission(api, authz.PermEventsRead),
101159
}, h.ListEvents)
102160

103-
huma.Register(api, huma.Operation{
104-
OperationID: "createEvent",
105-
Method: "POST",
106-
Path: "/events",
107-
Summary: "Create an event",
108-
Description: "Create a new system event",
109-
Tags: []string{"Events"},
110-
Security: []map[string][]string{
111-
{"BearerAuth": {}},
112-
{"ApiKeyAuth": {}},
113-
},
114-
// TODO: introduce a dedicated PermEventsCreate (and PermEventsDelete)
115-
// permission. Today the events taxonomy only exposes PermEventsRead,
116-
// so admin-level write actions reuse the read permission.
117-
Middlewares: humamw.RequirePermission(api, authz.PermEventsRead),
118-
}, h.CreateEvent)
119-
120161
huma.Register(api, huma.Operation{
121162
OperationID: "deleteEvent",
122163
Method: "DELETE",
@@ -128,10 +169,7 @@ func RegisterEvents(api huma.API, eventService *services.EventService, apiKeySvc
128169
{"BearerAuth": {}},
129170
{"ApiKeyAuth": {}},
130171
},
131-
// TODO: introduce a dedicated PermEventsDelete permission. Today the
132-
// events taxonomy only exposes PermEventsRead, so admin-level write
133-
// actions reuse the read permission.
134-
Middlewares: humamw.RequirePermission(api, authz.PermEventsRead),
172+
Middlewares: humamw.RequirePermission(api, authz.PermEventsDelete),
135173
}, h.DeleteEvent)
136174

137175
huma.Register(api, huma.Operation{
@@ -227,35 +265,6 @@ func (h *EventHandler) GetEventsByEnvironment(ctx context.Context, input *GetEve
227265
}, nil
228266
}
229267

230-
// CreateEvent creates a new event.
231-
func (h *EventHandler) CreateEvent(ctx context.Context, input *CreateEventInput) (*CreateEventOutput, error) {
232-
if h.eventService == nil {
233-
return nil, huma.Error500InternalServerError("service not available")
234-
}
235-
236-
if h.apiKeySvc != nil && input.XAPIKey != "" {
237-
resolvedEnvironmentID, err := h.apiKeySvc.GetEnvironmentByApiKey(ctx, input.XAPIKey)
238-
if err != nil {
239-
return nil, huma.Error401Unauthorized("invalid environment API key")
240-
}
241-
if resolvedEnvironmentID != nil && *resolvedEnvironmentID != "" {
242-
input.Body.EnvironmentID = new(*resolvedEnvironmentID)
243-
}
244-
}
245-
246-
evt, err := h.eventService.CreateEventFromDto(ctx, input.Body)
247-
if err != nil {
248-
return nil, huma.Error500InternalServerError((&common.EventCreationError{Err: err}).Error())
249-
}
250-
251-
return &CreateEventOutput{
252-
Body: base.ApiResponse[event.Event]{
253-
Success: true,
254-
Data: *evt,
255-
},
256-
}, nil
257-
}
258-
259268
// DeleteEvent deletes an event.
260269
func (h *EventHandler) DeleteEvent(ctx context.Context, input *DeleteEventInput) (*DeleteEventOutput, error) {
261270
if h.eventService == nil {

0 commit comments

Comments
 (0)