From 5c0056ef44f9db6f1176f4fd436b01f8f02f2b8e Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Thu, 23 Oct 2025 11:26:36 +0200 Subject: [PATCH] feat(openapi): Include HTTP handler for OpenAPI schema Signed-off-by: Javier Rodriguez --- app/controlplane/api/gen/openapi/embed.go | 21 ++++++++++++++ app/controlplane/internal/server/http.go | 6 +++- app/controlplane/internal/service/status.go | 31 +++++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 app/controlplane/api/gen/openapi/embed.go diff --git a/app/controlplane/api/gen/openapi/embed.go b/app/controlplane/api/gen/openapi/embed.go new file mode 100644 index 000000000..c8c88efe3 --- /dev/null +++ b/app/controlplane/api/gen/openapi/embed.go @@ -0,0 +1,21 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package openapi + +import _ "embed" + +//go:embed openapi.yaml +var Spec []byte diff --git a/app/controlplane/internal/server/http.go b/app/controlplane/internal/server/http.go index e4191c258..7e5e8c5f5 100644 --- a/app/controlplane/internal/server/http.go +++ b/app/controlplane/internal/server/http.go @@ -72,7 +72,8 @@ func NewHTTPServer(opts *Opts, grpcSrv *grpc.Server) (*http.Server, error) { opts.PrometheusSvc, ), )) - v1.RegisterStatusServiceHTTPServer(httpSrv, service.NewStatusService(opts.AuthSvc.AuthURLs.Login, Version, opts.CASClientUseCase, opts.BootstrapConfig)) + statusSvc := service.NewStatusService(opts.AuthSvc.AuthURLs.Login, Version, opts.CASClientUseCase, opts.BootstrapConfig) + v1.RegisterStatusServiceHTTPServer(httpSrv, statusSvc) v1.RegisterReferrerServiceHTTPServer(httpSrv, service.NewReferrerService(opts.ReferrerUseCase)) // Wrap http server to handle grpc-web calls and we will return this new server @@ -85,6 +86,9 @@ func NewHTTPServer(opts *Opts, grpcSrv *grpc.Server) (*http.Server, error) { r := httpSrv.Route("/") r.GET("/download/{digest}", opts.CASRedirectSvc.HTTPDownload) + // Include the OpenAPI spec handler + r.GET("/openapi.yaml", statusSvc.HandleOpenAPISpec) + // Handle grpc-web requests or fallback wrappedServer.Handler = h.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { if wrappedGrpc.IsGrpcWebRequest(req) || wrappedGrpc.IsAcceptableGrpcCorsRequest(req) { diff --git a/app/controlplane/internal/service/status.go b/app/controlplane/internal/service/status.go index 98e473579..67d8ce115 100644 --- a/app/controlplane/internal/service/status.go +++ b/app/controlplane/internal/service/status.go @@ -17,12 +17,16 @@ package service import ( "context" + "net/url" "os" + "strings" pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" + "github.com/chainloop-dev/chainloop/app/controlplane/api/gen/openapi" conf "github.com/chainloop-dev/chainloop/app/controlplane/internal/conf/controlplane/config/v1" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" "github.com/go-kratos/kratos/v2/errors" + khttp "github.com/go-kratos/kratos/v2/transport/http" ) type StatusService struct { @@ -54,3 +58,30 @@ func (s *StatusService) Infoz(_ context.Context, _ *pb.InfozRequest) (*pb.InfozR RestrictedOrgCreation: s.bootstrap.RestrictOrgCreation, }, nil } + +// HandleOpenAPISpec serves the OpenAPI specification with dynamic server URL +func (s *StatusService) HandleOpenAPISpec(ctx khttp.Context) error { + w := ctx.Response() + modifiedContent := string(openapi.Spec) + + // Get external URL from configuration and trim trailing slash if any + externalURL := strings.TrimRight(s.bootstrap.GetServer().GetHttp().GetExternalUrl(), "/") + + // Validate and sanitize external hostname before using it + if externalURL != "" { + // Parse URL to validate it's well-formed + parsedURL, err := url.Parse(externalURL) + if err == nil && parsedURL.Scheme != "" && parsedURL.Host != "" { + // Use validated URL for replacement + modifiedContent = strings.ReplaceAll(modifiedContent, "https://cp.chainloop.dev/", externalURL+"/") + } + // If invalid, just use the default (no replacement) + } + + // Return raw YAML with proper content type and security headers + w.Header().Set("Content-Type", "text/yaml; charset=UTF-8") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("Access-Control-Allow-Origin", "*") + _, _ = w.Write([]byte(modifiedContent)) + return nil +}