-
Notifications
You must be signed in to change notification settings - Fork 6
AEP Compliance Gap Analysis & Implementation Plan #42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,7 @@ import ( | |
| engineserver "github.com/dcm-project/policy-manager/internal/api/engine" | ||
| "github.com/dcm-project/policy-manager/internal/config" | ||
| "github.com/dcm-project/policy-manager/internal/logging" | ||
| custommiddleware "github.com/dcm-project/policy-manager/internal/middleware" | ||
| "github.com/go-chi/chi/v5" | ||
| "github.com/go-chi/chi/v5/middleware" | ||
| ) | ||
|
|
@@ -43,6 +44,7 @@ func (s *Server) Run(ctx context.Context) error { | |
| router.Use(logging.RequestLogger) | ||
| router.Use(middleware.Logger) | ||
| router.Use(middleware.Recoverer) | ||
| router.Use(custommiddleware.ProblemJSON) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): Same middleware ordering concern here: ProblemJSON may not affect error responses generated by Recoverer. To match the API server and ensure |
||
|
|
||
| swagger, err := engineserverapi.GetSwagger() | ||
| if err != nil { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ package v1alpha1 | |
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
|
|
||
| "github.com/dcm-project/policy-manager/api/v1alpha1" | ||
| "github.com/dcm-project/policy-manager/internal/api/server" | ||
|
|
@@ -62,9 +63,11 @@ func (h *PolicyHandler) CreatePolicy(ctx context.Context, request server.CreateP | |
| } | ||
|
|
||
| log.Info("Policy created", "policy_id", *created.Id) | ||
| // Convert back to server.Policy | ||
| return server.CreatePolicy201JSONResponse{ | ||
| Body: policyV1Alpha1ToServer(*created), | ||
| Headers: server.CreatePolicy201ResponseHeaders{ | ||
| Location: fmt.Sprintf("/api/v1alpha1/policies/%s", *created.Id), | ||
|
Comment on lines
+68
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): Consider URL-escaping the policy ID before embedding it into the Location header path. If |
||
| }, | ||
| }, nil | ||
| } | ||
|
|
||
|
|
@@ -107,7 +110,7 @@ func (h *PolicyHandler) ListPolicies(ctx context.Context, request server.ListPol | |
| return h.handleListPoliciesError(err, request), nil | ||
| } | ||
|
|
||
| log.Debug("ListPolicies completed", "count", len(result.Policies)) | ||
| log.Debug("ListPolicies completed", "count", len(result.Results)) | ||
| return server.ListPolicies200JSONResponse(listResponseV1Alpha1ToServer(*result)), nil | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package middleware | ||
|
|
||
| import ( | ||
| "net/http" | ||
| "strings" | ||
| ) | ||
|
|
||
| // ProblemJSON is a Chi middleware that rewrites the Content-Type header | ||
| // from application/json to application/problem+json for error responses | ||
| // (status >= 400), per AEP-193 and RFC 9457. | ||
| func ProblemJSON(next http.Handler) http.Handler { | ||
| return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| next.ServeHTTP(&problemJSONWriter{ResponseWriter: w}, r) | ||
| }) | ||
| } | ||
|
|
||
| type problemJSONWriter struct { | ||
| http.ResponseWriter | ||
| wroteHeader bool | ||
| } | ||
|
|
||
| func (w *problemJSONWriter) WriteHeader(code int) { | ||
| if !w.wroteHeader && code >= 400 { | ||
| ct := w.Header().Get("Content-Type") | ||
| if strings.HasPrefix(ct, "application/json") { | ||
| w.Header().Set("Content-Type", strings.Replace(ct, "application/json", "application/problem+json", 1)) | ||
|
Comment on lines
+17
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): The ResponseWriter wrapper does not forward optional interfaces (Flusher, Hijacker, Pusher, etc.), which may break callers that rely on them. Since this wrapper only embeds |
||
| } | ||
| } | ||
| w.wroteHeader = true | ||
| w.ResponseWriter.WriteHeader(code) | ||
| } | ||
|
|
||
| func (w *problemJSONWriter) Write(b []byte) (int, error) { | ||
| if !w.wroteHeader { | ||
| w.WriteHeader(http.StatusOK) | ||
| } | ||
| return w.ResponseWriter.Write(b) | ||
| } | ||
|
|
||
| func (w *problemJSONWriter) Unwrap() http.ResponseWriter { | ||
| return w.ResponseWriter | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| package middleware | ||
|
|
||
| import ( | ||
| "net/http" | ||
| "net/http/httptest" | ||
| "testing" | ||
|
|
||
| . "github.com/onsi/ginkgo/v2" | ||
| . "github.com/onsi/gomega" | ||
| ) | ||
|
|
||
| func TestMiddleware(t *testing.T) { | ||
| RegisterFailHandler(Fail) | ||
| RunSpecs(t, "Middleware Suite") | ||
| } | ||
|
|
||
| var _ = Describe("ProblemJSON", func() { | ||
| makeHandler := func(status int, contentType string) http.Handler { | ||
| return ProblemJSON(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | ||
| w.Header().Set("Content-Type", contentType) | ||
| w.WriteHeader(status) | ||
| w.Write([]byte(`{"type":"INTERNAL","status":500,"title":"error"}`)) | ||
| })) | ||
| } | ||
|
|
||
| It("should rewrite Content-Type for 400 responses", func() { | ||
| rec := httptest.NewRecorder() | ||
| makeHandler(400, "application/json").ServeHTTP(rec, httptest.NewRequest("GET", "/", nil)) | ||
| Expect(rec.Header().Get("Content-Type")).To(Equal("application/problem+json")) | ||
|
Comment on lines
+26
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (testing): Add an E2E test to verify error responses use application/problem+json Since the implementation plan also called out an end-to-end check, please add an E2E test (e.g., triggering a known 4xx/5xx route) to confirm the middleware is wired into both API servers and that the Suggested implementation: It("should rewrite Content-Type for 400 responses", func() {
rec := httptest.NewRecorder()
makeHandler(400, "application/json").ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
Expect(rec.Header().Get("Content-Type")).To(Equal("application/problem+json"))
})
It("should preserve application/problem+json Content-Type in an end-to-end HTTP request", func() {
// Arrange: wrap a real HTTP handler with the ProblemJSON middleware and serve via httptest.Server
handler := ProblemJSON(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Simulate a handler that returns a problem+json 400 response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(`{"type":"INTERNAL","status":400,"title":"bad request"}`))
}))
server := httptest.NewServer(handler)
defer server.Close()
// Act: perform a real HTTP request against the server
resp, err := http.Get(server.URL)
Expect(err).NotTo(HaveOccurred())
defer resp.Body.Close()
// Assert: middleware has rewritten the Content-Type header on the actual HTTP response
Expect(resp.StatusCode).To(Equal(http.StatusBadRequest))
Expect(resp.Header.Get("Content-Type")).To(Equal("application/problem+json"))
})If your actual API servers are constructed via helper functions (e.g.,
|
||
| }) | ||
|
|
||
| It("should rewrite Content-Type for 500 responses", func() { | ||
| rec := httptest.NewRecorder() | ||
| makeHandler(500, "application/json").ServeHTTP(rec, httptest.NewRequest("GET", "/", nil)) | ||
| Expect(rec.Header().Get("Content-Type")).To(Equal("application/problem+json")) | ||
| }) | ||
|
|
||
| It("should preserve Content-Type for 200 responses", func() { | ||
| rec := httptest.NewRecorder() | ||
| handler := ProblemJSON(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | ||
| w.Header().Set("Content-Type", "application/json") | ||
| w.WriteHeader(200) | ||
| w.Write([]byte(`{"status":"ok"}`)) | ||
| })) | ||
| handler.ServeHTTP(rec, httptest.NewRequest("GET", "/", nil)) | ||
| Expect(rec.Header().Get("Content-Type")).To(Equal("application/json")) | ||
| }) | ||
|
|
||
| It("should preserve Content-Type with charset for 400 responses", func() { | ||
| rec := httptest.NewRecorder() | ||
| makeHandler(404, "application/json; charset=utf-8").ServeHTTP(rec, httptest.NewRequest("GET", "/", nil)) | ||
| Expect(rec.Header().Get("Content-Type")).To(Equal("application/problem+json; charset=utf-8")) | ||
| }) | ||
|
|
||
| It("should not touch non-JSON content types on errors", func() { | ||
| rec := httptest.NewRecorder() | ||
| makeHandler(500, "text/plain").ServeHTTP(rec, httptest.NewRequest("GET", "/", nil)) | ||
| Expect(rec.Header().Get("Content-Type")).To(Equal("text/plain")) | ||
| }) | ||
| }) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): The ProblemJSON middleware order likely prevents it from rewriting responses generated by Recoverer (e.g., panics).
Because of Chi’s middleware wrapping order, adding
ProblemJSONaftermiddleware.Recoverermeans panics are handled byRecovererusing the originalResponseWriter, soProblemJSONnever sees or rewrites those 500 responses. That leaves panic/500 responses without theapplication/problem+jsonContent-Type, which may violate AEP-193. To cover all errors, including those fromRecoverer, registerProblemJSONbeforeRecovererso it wraps the full stack.