From 64c64c24095602bba05357ac903ac72fe86219f1 Mon Sep 17 00:00:00 2001 From: ManthanNimodiya Date: Fri, 18 Sep 2026 18:51:37 +0530 Subject: [PATCH 1/2] feat(server): expose openapi and swagger endpoints on http server Expose OpenAPI 3.0 and Swagger 2.0 specs at /openapi.json, /swagger.json, and /docs endpoints on the HTTP server. Fixes #1714 Signed-off-by: ManthanNimodiya --- docs/docs.go | 20 ++++++ internal/servers/openapi.go | 35 +++++++++++ internal/servers/openapi_test.go | 101 +++++++++++++++++++++++++++++++ internal/servers/server.go | 3 + 4 files changed, 159 insertions(+) create mode 100644 docs/docs.go create mode 100644 internal/servers/openapi.go create mode 100644 internal/servers/openapi_test.go diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 000000000..809b3f94c --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,20 @@ +package docs + +import ( + _ "embed" +) + +// OpenAPIJSON holds the OpenAPI 3.0 JSON specification. +// +//go:embed api-reference/openapi.json +var OpenAPIJSON []byte + +// SwaggerJSON holds the Swagger 2.0 / OpenAPI 2.0 JSON specification. +// +//go:embed api-reference/apidocs.swagger.json +var SwaggerJSON []byte + +// OpenAPIV2JSON holds the OpenAPI 2.0 JSON specification. +// +//go:embed api-reference/openapiv2/apidocs.swagger.json +var OpenAPIV2JSON []byte diff --git a/internal/servers/openapi.go b/internal/servers/openapi.go new file mode 100644 index 000000000..36241a04f --- /dev/null +++ b/internal/servers/openapi.go @@ -0,0 +1,35 @@ +package servers + +import ( + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + + "github.com/Permify/permify/docs" +) + +// RegisterOpenAPIHandlers registers endpoints on the gRPC-Gateway ServeMux to expose +// OpenAPI and Swagger specifications over HTTP. +func RegisterOpenAPIHandlers(mux *runtime.ServeMux) error { + endpoints := map[string][]byte{ + "/openapi.json": docs.OpenAPIJSON, + "/swagger.json": docs.SwaggerJSON, + "/docs/openapi.json": docs.OpenAPIJSON, + "/docs/swagger.json": docs.SwaggerJSON, + "/docs/openapiv2.json": docs.OpenAPIV2JSON, + } + + for path, spec := range endpoints { + content := spec + err := mux.HandlePath(http.MethodGet, path, func(w http.ResponseWriter, r *http.Request, _ map[string]string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(content) + }) + if err != nil { + return err + } + } + + return nil +} diff --git a/internal/servers/openapi_test.go b/internal/servers/openapi_test.go new file mode 100644 index 000000000..6f59668fc --- /dev/null +++ b/internal/servers/openapi_test.go @@ -0,0 +1,101 @@ +package servers + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/stretchr/testify/require" +) + +func TestRegisterOpenAPIHandlers(t *testing.T) { + mux := runtime.NewServeMux() + err := RegisterOpenAPIHandlers(mux) + require.NoError(t, err) + + testCases := []struct { + name string + path string + expectedCode int + validateJSON func(t *testing.T, body []byte) + }{ + { + name: "GET /openapi.json", + path: "/openapi.json", + expectedCode: http.StatusOK, + validateJSON: func(t *testing.T, body []byte) { + var data map[string]any + err := json.Unmarshal(body, &data) + require.NoError(t, err) + require.Equal(t, "3.0.0", data["openapi"]) + info, ok := data["info"].(map[string]any) + require.True(t, ok) + require.Equal(t, "Permify API", info["title"]) + }, + }, + { + name: "GET /swagger.json", + path: "/swagger.json", + expectedCode: http.StatusOK, + validateJSON: func(t *testing.T, body []byte) { + var data map[string]any + err := json.Unmarshal(body, &data) + require.NoError(t, err) + require.Equal(t, "2.0", data["swagger"]) + info, ok := data["info"].(map[string]any) + require.True(t, ok) + require.Equal(t, "Permify API", info["title"]) + }, + }, + { + name: "GET /docs/openapi.json", + path: "/docs/openapi.json", + expectedCode: http.StatusOK, + validateJSON: func(t *testing.T, body []byte) { + var data map[string]any + err := json.Unmarshal(body, &data) + require.NoError(t, err) + require.Equal(t, "3.0.0", data["openapi"]) + }, + }, + { + name: "GET /docs/swagger.json", + path: "/docs/swagger.json", + expectedCode: http.StatusOK, + validateJSON: func(t *testing.T, body []byte) { + var data map[string]any + err := json.Unmarshal(body, &data) + require.NoError(t, err) + require.Equal(t, "2.0", data["swagger"]) + }, + }, + { + name: "GET /docs/openapiv2.json", + path: "/docs/openapiv2.json", + expectedCode: http.StatusOK, + validateJSON: func(t *testing.T, body []byte) { + var data map[string]any + err := json.Unmarshal(body, &data) + require.NoError(t, err) + require.Equal(t, "2.0", data["swagger"]) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, tc.path, nil) + w := httptest.NewRecorder() + + mux.ServeHTTP(w, req) + + require.Equal(t, tc.expectedCode, w.Code) + require.Equal(t, "application/json", w.Header().Get("Content-Type")) + if tc.validateJSON != nil { + tc.validateJSON(t, w.Body.Bytes()) + } + }) + } +} diff --git a/internal/servers/server.go b/internal/servers/server.go index 6c7fa525f..1a4779f9a 100644 --- a/internal/servers/server.go +++ b/internal/servers/server.go @@ -329,6 +329,9 @@ func (s *Container) Run( if err = grpcV1.RegisterTenancyHandler(ctx, mux, conn); err != nil { return err } + if err = RegisterOpenAPIHandlers(mux); err != nil { + return err + } corsHandler := cors.New(cors.Options{ // CORS configuration AllowCredentials: true, // Allow credentials From e8347d101615269717e2136e4189f965dc293470 Mon Sep 17 00:00:00 2001 From: ManthanNimodiya Date: Sat, 19 Sep 2026 07:53:07 +0530 Subject: [PATCH 2/2] refactor(server): scope openapi endpoint to /openapi.json Remove extra Swagger aliases and unused embeddings to keep the endpoint strictly scoped. Signed-off-by: ManthanNimodiya --- docs/docs.go | 12 +--- internal/servers/openapi.go | 30 +++------ internal/servers/openapi_test.go | 103 +++++++------------------------ 3 files changed, 31 insertions(+), 114 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 809b3f94c..d59f9b490 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -4,17 +4,7 @@ import ( _ "embed" ) -// OpenAPIJSON holds the OpenAPI 3.0 JSON specification. +// OpenAPIJSON holds the raw OpenAPI 3.0 JSON specification for the Permify API. // //go:embed api-reference/openapi.json var OpenAPIJSON []byte - -// SwaggerJSON holds the Swagger 2.0 / OpenAPI 2.0 JSON specification. -// -//go:embed api-reference/apidocs.swagger.json -var SwaggerJSON []byte - -// OpenAPIV2JSON holds the OpenAPI 2.0 JSON specification. -// -//go:embed api-reference/openapiv2/apidocs.swagger.json -var OpenAPIV2JSON []byte diff --git a/internal/servers/openapi.go b/internal/servers/openapi.go index 36241a04f..60c79c827 100644 --- a/internal/servers/openapi.go +++ b/internal/servers/openapi.go @@ -8,28 +8,12 @@ import ( "github.com/Permify/permify/docs" ) -// RegisterOpenAPIHandlers registers endpoints on the gRPC-Gateway ServeMux to expose -// OpenAPI and Swagger specifications over HTTP. +// RegisterOpenAPIHandlers registers the OpenAPI endpoint on the gRPC-Gateway ServeMux +// to expose the API specification over HTTP. func RegisterOpenAPIHandlers(mux *runtime.ServeMux) error { - endpoints := map[string][]byte{ - "/openapi.json": docs.OpenAPIJSON, - "/swagger.json": docs.SwaggerJSON, - "/docs/openapi.json": docs.OpenAPIJSON, - "/docs/swagger.json": docs.SwaggerJSON, - "/docs/openapiv2.json": docs.OpenAPIV2JSON, - } - - for path, spec := range endpoints { - content := spec - err := mux.HandlePath(http.MethodGet, path, func(w http.ResponseWriter, r *http.Request, _ map[string]string) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write(content) - }) - if err != nil { - return err - } - } - - return nil + return mux.HandlePath(http.MethodGet, "/openapi.json", func(w http.ResponseWriter, r *http.Request, _ map[string]string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(docs.OpenAPIJSON) + }) } diff --git a/internal/servers/openapi_test.go b/internal/servers/openapi_test.go index 6f59668fc..6c8f6c341 100644 --- a/internal/servers/openapi_test.go +++ b/internal/servers/openapi_test.go @@ -15,87 +15,30 @@ func TestRegisterOpenAPIHandlers(t *testing.T) { err := RegisterOpenAPIHandlers(mux) require.NoError(t, err) - testCases := []struct { - name string - path string - expectedCode int - validateJSON func(t *testing.T, body []byte) - }{ - { - name: "GET /openapi.json", - path: "/openapi.json", - expectedCode: http.StatusOK, - validateJSON: func(t *testing.T, body []byte) { - var data map[string]any - err := json.Unmarshal(body, &data) - require.NoError(t, err) - require.Equal(t, "3.0.0", data["openapi"]) - info, ok := data["info"].(map[string]any) - require.True(t, ok) - require.Equal(t, "Permify API", info["title"]) - }, - }, - { - name: "GET /swagger.json", - path: "/swagger.json", - expectedCode: http.StatusOK, - validateJSON: func(t *testing.T, body []byte) { - var data map[string]any - err := json.Unmarshal(body, &data) - require.NoError(t, err) - require.Equal(t, "2.0", data["swagger"]) - info, ok := data["info"].(map[string]any) - require.True(t, ok) - require.Equal(t, "Permify API", info["title"]) - }, - }, - { - name: "GET /docs/openapi.json", - path: "/docs/openapi.json", - expectedCode: http.StatusOK, - validateJSON: func(t *testing.T, body []byte) { - var data map[string]any - err := json.Unmarshal(body, &data) - require.NoError(t, err) - require.Equal(t, "3.0.0", data["openapi"]) - }, - }, - { - name: "GET /docs/swagger.json", - path: "/docs/swagger.json", - expectedCode: http.StatusOK, - validateJSON: func(t *testing.T, body []byte) { - var data map[string]any - err := json.Unmarshal(body, &data) - require.NoError(t, err) - require.Equal(t, "2.0", data["swagger"]) - }, - }, - { - name: "GET /docs/openapiv2.json", - path: "/docs/openapiv2.json", - expectedCode: http.StatusOK, - validateJSON: func(t *testing.T, body []byte) { - var data map[string]any - err := json.Unmarshal(body, &data) - require.NoError(t, err) - require.Equal(t, "2.0", data["swagger"]) - }, - }, - } + t.Run("GET /openapi.json returns 200 and valid OpenAPI JSON", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/openapi.json", nil) + w := httptest.NewRecorder() - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, tc.path, nil) - w := httptest.NewRecorder() + mux.ServeHTTP(w, req) - mux.ServeHTTP(w, req) + require.Equal(t, http.StatusOK, w.Code) + require.Equal(t, "application/json", w.Header().Get("Content-Type")) - require.Equal(t, tc.expectedCode, w.Code) - require.Equal(t, "application/json", w.Header().Get("Content-Type")) - if tc.validateJSON != nil { - tc.validateJSON(t, w.Body.Bytes()) - } - }) - } + var data map[string]any + err := json.Unmarshal(w.Body.Bytes(), &data) + require.NoError(t, err) + require.Equal(t, "3.0.0", data["openapi"]) + info, ok := data["info"].(map[string]any) + require.True(t, ok) + require.Equal(t, "Permify API", info["title"]) + }) + + t.Run("GET /unregistered returns 404", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/unregistered", nil) + w := httptest.NewRecorder() + + mux.ServeHTTP(w, req) + + require.Equal(t, http.StatusNotFound, w.Code) + }) }