Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 17 additions & 10 deletions apps/manager-server/internal/http/controller/proxy/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,21 +39,28 @@ func (h *Handler) ModelList(w http.ResponseWriter, r *http.Request) {
}

func (h *Handler) CPAResource(w http.ResponseWriter, r *http.Request) {
useSavedManagementKey := true
switch r.Method {
case http.MethodGet, http.MethodHead:
case http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete:
ok, err := h.App.AdminAuthService.VerifyHeader(r.Context(), r.Header.Get("Authorization"))
if err != nil {
response.Error(w, http.StatusInternalServerError, err)
return
}
useSavedManagementKey = ok
case http.MethodGet,
http.MethodHead,
http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete:
default:
response.MethodNotAllowed(w)
return
}
if useSavedManagementKey {

ok, err := h.App.AdminAuthService.VerifyHeader(r.Context(), r.Header.Get("Authorization"))
if err != nil {
response.Error(w, http.StatusInternalServerError, err)
return
}
// Only a verified CPAMP admin key authorizes substituting the saved CPA
// management key. Otherwise preserve the caller's own authorization (or
// lack of it) so public plugin resources remain reachable without privilege
// elevation.
if ok {
h.App.ProxyService.ProxyPluginResource(w, r, response.Error)
return
}
Expand Down
52 changes: 48 additions & 4 deletions apps/manager-server/internal/httpapi/server_compat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -755,7 +755,7 @@ func TestServerCompatPluginProxyRoutes(t *testing.T) {
body string
}

observed := make(chan observedRequest, 9)
observed := make(chan observedRequest, 12)
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
observed <- observedRequest{
Expand Down Expand Up @@ -954,7 +954,9 @@ func TestServerCompatPluginProxyRoutes(t *testing.T) {
)
testutil.RequireStatus(t, resourceTraceRR, http.StatusMethodNotAllowed)

resourceRR := requestWithHeaders(
// Public plugin resources may remain reachable, but an unauthenticated
// caller must never be elevated to the saved CPA management key.
resourceNoAuthRR := requestWithHeaders(
http.MethodGet,
"/v0/resource/plugins/codex-invite/invite",
"",
Expand All @@ -963,13 +965,55 @@ func TestServerCompatPluginProxyRoutes(t *testing.T) {
"X-Codex-Invite-Origin": "http://localhost:18317",
},
)
testutil.RequireStatus(t, resourceRR, http.StatusOK)
testutil.RequireStatus(t, resourceNoAuthRR, http.StatusOK)
assertObserved("/v0/resource/plugins/codex-invite/invite", observedRequest{
method: http.MethodGet,
path: "/v0/resource/plugins/codex-invite/invite",
authorization: "Bearer management-key",
codexInviteOrigin: upstream.URL,
})

resourceCallerAuthRR := requestWithHeaders(
http.MethodGet,
"/v0/resource/plugins/codex-invite/invite",
"",
"plugin-management-key",
nil,
)
testutil.RequireStatus(t, resourceCallerAuthRR, http.StatusOK)
assertObserved("/v0/resource/plugins/codex-invite/invite", observedRequest{
method: http.MethodGet,
path: "/v0/resource/plugins/codex-invite/invite",
authorization: "Bearer plugin-management-key",
})

resourceAdminAuthRR := testutil.Request(
t,
handler,
http.MethodGet,
"/v0/resource/plugins/codex-invite/invite",
"",
testutil.AdminKey,
)
testutil.RequireStatus(t, resourceAdminAuthRR, http.StatusOK)
assertObserved("/v0/resource/plugins/codex-invite/invite", observedRequest{
method: http.MethodGet,
path: "/v0/resource/plugins/codex-invite/invite",
authorization: "Bearer management-key",
})

resourceHeadNoAuthRR := testutil.Request(
t,
handler,
http.MethodHead,
"/v0/resource/plugins/codex-invite/invite",
"",
"",
)
testutil.RequireStatus(t, resourceHeadNoAuthRR, http.StatusOK)
assertObserved("/v0/resource/plugins/codex-invite/invite", observedRequest{
method: http.MethodHead,
path: "/v0/resource/plugins/codex-invite/invite",
})
}

type recordingAutomationRuntimeService struct {
Expand Down
Loading