mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add API to serve proxy certificate (#21391)
Closes https://github.com/coder/internal/issues/1184
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
package coderd
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
// RegisterInMemoryAIBridgeProxydHTTPHandler mounts [aibridgeproxyd.Server]'s HTTP handler
|
||||
// onto [API]'s router, so that requests to aibridgedproxy will be relayed from Coder's API server
|
||||
// to the in-memory aibridgedproxy.
|
||||
func (api *API) RegisterInMemoryAIBridgeProxydHTTPHandler(srv http.Handler) {
|
||||
if srv == nil {
|
||||
panic("aibridgeproxyd cannot be nil")
|
||||
}
|
||||
|
||||
api.aibridgeproxydHandler = srv
|
||||
}
|
||||
|
||||
// aibridgeproxyHandler handles AI Bridge Proxy endpoints.
|
||||
func aibridgeproxyHandler(api *API, middlewares ...func(http.Handler) http.Handler) func(r chi.Router) {
|
||||
return func(r chi.Router) {
|
||||
r.Use(api.RequireFeatureMW(codersdk.FeatureAIBridge))
|
||||
r.Use(middlewares...)
|
||||
|
||||
r.HandleFunc("/*", func(rw http.ResponseWriter, r *http.Request) {
|
||||
// Check if the proxy is enabled.
|
||||
if !api.DeploymentValues.AI.BridgeProxyConfig.Enabled.Value() {
|
||||
httpapi.Write(r.Context(), rw, http.StatusNotFound, codersdk.Response{
|
||||
Message: "AI Bridge Proxy is not enabled.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the handler is registered.
|
||||
if api.aibridgeproxydHandler == nil {
|
||||
httpapi.Write(r.Context(), rw, http.StatusNotFound, codersdk.Response{
|
||||
Message: "AI Bridge Proxy handler not mounted.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Strip the prefix and relay to the aibridgeproxyd handler.
|
||||
http.StripPrefix("/api/v2/aibridge/proxy", api.aibridgeproxydHandler).ServeHTTP(rw, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package coderd_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/license"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func TestAIBridgeProxyCertificateRetrieval(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("DisabledReturns404", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dv := coderdtest.DeploymentValues(t)
|
||||
// Proxy is disabled by default, so we don't need to set it explicitly.
|
||||
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
DeploymentValues: dv,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAIBridge: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// Make a request to the proxy CA cert endpoint.
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, client.URL.String()+"/api/v2/aibridge/proxy/ca-cert.pem", nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set(codersdk.SessionTokenHeader, client.SessionToken())
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("RequiresLicenseFeature", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dv := coderdtest.DeploymentValues(t)
|
||||
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
DeploymentValues: dv,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
// No aibridge feature.
|
||||
Features: license.Features{},
|
||||
},
|
||||
})
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// Make a request to the proxy CA cert endpoint.
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, client.URL.String()+"/api/v2/aibridge/proxy/ca-cert.pem", nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set(codersdk.SessionTokenHeader, client.SessionToken())
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("RequiresAuthentication", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dv := coderdtest.DeploymentValues(t)
|
||||
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
DeploymentValues: dv,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAIBridge: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// Make a request to the proxy CA cert endpoint without authentication.
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, client.URL.String()+"/api/v2/aibridge/proxy/ca-cert.pem", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// No session token header set.
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
@@ -230,6 +230,10 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
||||
r.Route("/aibridge", aibridgeHandler(api, apiKeyMiddleware))
|
||||
})
|
||||
|
||||
api.AGPL.APIHandler.Group(func(r chi.Router) {
|
||||
r.Route("/aibridge/proxy", aibridgeproxyHandler(api, apiKeyMiddleware))
|
||||
})
|
||||
|
||||
api.AGPL.APIHandler.Group(func(r chi.Router) {
|
||||
r.Get("/entitlements", api.serveEntitlements)
|
||||
// /regions overrides the AGPL /regions endpoint
|
||||
@@ -691,7 +695,8 @@ type API struct {
|
||||
licenseMetricsCollector *license.MetricsCollector
|
||||
tailnetService *tailnet.ClientService
|
||||
|
||||
aibridgedHandler http.Handler
|
||||
aibridgedHandler http.Handler
|
||||
aibridgeproxydHandler http.Handler
|
||||
}
|
||||
|
||||
// writeEntitlementWarningsHeader writes the entitlement warnings to the response header
|
||||
|
||||
Reference in New Issue
Block a user