From 0c7a78a0b26e8ecfe39cf1ac8c4eaa16a2ae97c1 Mon Sep 17 00:00:00 2001 From: Dinith Herath Date: Fri, 10 Jul 2026 18:42:48 +0530 Subject: [PATCH 1/5] Allow API key creation without an existing gateway association --- platform-api/internal/service/apikey.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/platform-api/internal/service/apikey.go b/platform-api/internal/service/apikey.go index a2a562baa..795207259 100644 --- a/platform-api/internal/service/apikey.go +++ b/platform-api/internal/service/apikey.go @@ -297,9 +297,6 @@ func (s *APIKeyService) CreateAPIKey(ctx context.Context, apiHandle, kind, orgId if err != nil { return fmt.Errorf("failed to get API deployments for API handle: %s: %w", apiHandle, err) } - if len(gateways) == 0 { - return apperror.GatewayConnectionUnavailable.Wrap(constants.ErrGatewayUnavailable) - } // Resolve key name (required for DB uniqueness; derive from request or generate) keyName, err := s.resolveUniqueKeyName(apiId, req, apiHandle) From c79d39579076993801481e5d254569aef2079c25 Mon Sep 17 00:00:00 2001 From: Dinith Herath Date: Fri, 10 Jul 2026 18:44:59 +0530 Subject: [PATCH 2/5] Push existing API keys to a gateway on artifact deployment --- platform-api/internal/server/server.go | 9 +- platform-api/internal/service/apikey.go | 119 +++++++++-- .../service/artifact_dp_apikey_test.go | 98 ++++++++- platform-api/internal/service/deployment.go | 19 ++ .../deployment_apikey_backfill_test.go | 188 ++++++++++++++++++ .../internal/service/llm_deployment.go | 20 ++ .../internal/service/mcp_deployment.go | 10 +- platform-api/plugins/eventgateway/plugin.go | 2 + .../service/webbroker_api_deployment.go | 9 + .../service/websub_api_deployment.go | 9 + 10 files changed, 455 insertions(+), 28 deletions(-) create mode 100644 platform-api/internal/service/deployment_apikey_backfill_test.go diff --git a/platform-api/internal/server/server.go b/platform-api/internal/server/server.go index 148aaf526..716b08264 100644 --- a/platform-api/internal/server/server.go +++ b/platform-api/internal/server/server.go @@ -258,7 +258,7 @@ func StartPlatformAPIServer(cfg *config.Server, slogger *slog.Logger) (*Server, subscriptionPlanService := service.NewSubscriptionPlanService(subscriptionPlanRepo, gatewayRepo, orgRepo, gatewayEventsService, auditRepo, slogger) internalGatewayService := service.NewGatewayInternalAPIService(apiRepo, subscriptionRepo, subscriptionPlanRepo, llmProviderRepo, llmProxyRepo, mcpProxyRepo, deploymentRepo, gatewayRepo, orgRepo, projectRepo, apiKeyRepo, artifactRepo, secretRepo, cfg, slogger) apiKeyService := service.NewAPIKeyService(apiRepo, artifactRepo, apiKeyRepo, gatewayEventsService, auditRepo, cfg.APIKey.HashingAlgorithms, slogger) - deploymentService := service.NewDeploymentService(apiRepo, artifactRepo, deploymentRepo, gatewayRepo, orgRepo, gatewayEventsService, auditRepo, apiUtil, cfg, slogger) + deploymentService := service.NewDeploymentService(apiRepo, artifactRepo, deploymentRepo, gatewayRepo, orgRepo, apiKeyRepo, gatewayEventsService, auditRepo, apiUtil, cfg, slogger) llmTemplateService := service.NewLLMProviderTemplateService(llmTemplateRepo, auditRepo, identityService) llmProviderService := service.NewLLMProviderService(llmProviderRepo, llmTemplateRepo, orgRepo, llmTemplateSeeder, deploymentRepo, gatewayRepo, gatewayEventsService, slogger, auditRepo, cfg, identityService) llmProxyService := service.NewLLMProxyService(llmProxyRepo, llmProviderRepo, projectRepo, deploymentRepo, gatewayRepo, gatewayEventsService, slogger, auditRepo, cfg, identityService) @@ -279,18 +279,20 @@ func StartPlatformAPIServer(cfg *config.Server, slogger *slog.Logger) (*Server, deploymentRepo, gatewayRepo, orgRepo, + apiKeyRepo, gatewayEventsService, cfg, slogger, ) - llmProviderAPIKeyService := service.NewLLMProviderAPIKeyService(llmProviderRepo, gatewayRepo, apiKeyRepo, gatewayEventsService, identityService, slogger) - llmProxyAPIKeyService := service.NewLLMProxyAPIKeyService(llmProxyRepo, gatewayRepo, apiKeyRepo, gatewayEventsService, identityService, slogger) + llmProviderAPIKeyService := service.NewLLMProviderAPIKeyService(llmProviderRepo, apiRepo, apiKeyRepo, gatewayEventsService, identityService, slogger) + llmProxyAPIKeyService := service.NewLLMProxyAPIKeyService(llmProxyRepo, apiRepo, apiKeyRepo, gatewayEventsService, identityService, slogger) apiKeyUserService := service.NewAPIKeyUserService(apiKeyRepo, identityService, slogger) llmProxyDeploymentService := service.NewLLMProxyDeploymentService( llmProxyRepo, deploymentRepo, gatewayRepo, orgRepo, + apiKeyRepo, gatewayEventsService, cfg, slogger, @@ -301,6 +303,7 @@ func StartPlatformAPIServer(cfg *config.Server, slogger *slog.Logger) (*Server, gatewayRepo, orgRepo, artifactRepo, + apiKeyRepo, gatewayEventsService, cfg, slogger, diff --git a/platform-api/internal/service/apikey.go b/platform-api/internal/service/apikey.go index 795207259..0e3f1b210 100644 --- a/platform-api/internal/service/apikey.go +++ b/platform-api/internal/service/apikey.go @@ -179,6 +179,26 @@ func filterGatewaysByAllowedTargets(gateways []*model.Gateway, allowedTargets st return filtered } +// filterAPIGatewaysByAllowedTargets is filterGatewaysByAllowedTargets for the +// association-scoped []*model.APIGatewayWithDetails shape returned by +// GetAPIGatewaysWithDetails. Matching is by gateway Name, identical to the base helper. +func filterAPIGatewaysByAllowedTargets(gateways []*model.APIGatewayWithDetails, allowedTargets string) []*model.APIGatewayWithDetails { + if allowedTargets == "" || allowedTargets == constants.APIKeyAllowedTargetsAll { + return gateways + } + allowed := make(map[string]struct{}) + for _, name := range strings.Split(allowedTargets, ",") { + allowed[strings.TrimSpace(name)] = struct{}{} + } + filtered := make([]*model.APIGatewayWithDetails, 0, len(gateways)) + for _, gw := range gateways { + if _, ok := allowed[gw.Name]; ok { + filtered = append(filtered, gw) + } + } + return filtered +} + // randomHexString generates a random lowercase hex string of the requested length. func randomHexString(n int) (string, error) { bytes := make([]byte, (n+1)/2) @@ -276,6 +296,84 @@ func (s *APIKeyService) resolveUniqueKeyName(artifactUUID string, req *api.Creat return "", fmt.Errorf("failed to generate a unique API key name after %d retries", maxRetries) } +// APIKeyCreatedEventFromModel builds an apikey.created broadcast payload from a +// persisted API key record. It sends the hash JSON and masked key, never the plain +// key. Used both at key-creation time and to (re)broadcast pre-existing keys to a +// gateway the API is newly associated with at deploy time. +func APIKeyCreatedEventFromModel(k *model.APIKey) *model.APIKeyCreatedEvent { + event := &model.APIKeyCreatedEvent{ + UUID: k.UUID, + ApiId: k.ArtifactUUID, + Name: k.Name, + ApiKeyHashes: k.APIKeyHashes, + MaskedApiKey: k.MaskedAPIKey, + Issuer: k.Issuer, + CreatedAt: k.CreatedAt.Format(time.RFC3339), + UpdatedAt: k.UpdatedAt.Format(time.RFC3339), + } + if k.ExpiresAt != nil { + expiresAtStr := k.ExpiresAt.Format(time.RFC3339) + event.ExpiresAt = &expiresAtStr + } + return event +} + +// BackfillAPIKeysToGateway (re)broadcasts an artifact's existing active API keys to a +// gateway it has just been deployed/associated to. Keys are broadcast to their associated +// gateways only once, at creation time (CreateAPIKey and the per-kind key services), so a +// key created before this association would otherwise reach the gateway only after the +// gateway-controller's next reconnect bulk sync — leaving a window where the artifact is +// live but the key is rejected. Pushing existing keys here closes that window. +// +// It is artifact-kind-agnostic: ListByArtifact and the apikey.created event/controller +// handler are the same for REST APIs, LLM providers/proxies, MCP proxies, and WebSub/ +// WebBroker APIs, so every deploy path can share this one helper. +// +// Best-effort: the controller upserts apikey.created idempotently, so re-sending a key the +// gateway already holds is harmless, and any failure is logged without blocking the +// deployment (the reconnect bulk sync remains the safety net). +func BackfillAPIKeysToGateway(apiKeyRepo repository.APIKeyRepository, events *GatewayEventsService, slogger *slog.Logger, artifactUUID, gatewayID, actor string) { + if events == nil || apiKeyRepo == nil { + return + } + + keys, err := apiKeyRepo.ListByArtifact(artifactUUID) + if err != nil { + if slogger != nil { + slogger.Warn("Failed to load API keys for deploy-time backfill", + "artifactId", artifactUUID, "gatewayId", gatewayID, "error", err) + } + return + } + + now := time.Now() + backfilled := 0 + for _, k := range keys { + if k == nil || k.Status != constants.APIKeyStatusActive { + continue + } + // Skip expired keys — never push a dead key to a gateway. + if k.ExpiresAt != nil && !k.ExpiresAt.After(now) { + continue + } + + event := APIKeyCreatedEventFromModel(k) + if err := events.BroadcastAPIKeyCreatedEvent(gatewayID, actor, event); err != nil { + if slogger != nil { + slogger.Warn("Failed to backfill API key to gateway", + "artifactId", artifactUUID, "gatewayId", gatewayID, "keyName", k.Name, "error", err) + } + continue + } + backfilled++ + } + + if backfilled > 0 && slogger != nil { + slogger.Info("Backfilled existing API keys to gateway at deploy time", + "artifactId", artifactUUID, "gatewayId", gatewayID, "count", backfilled) + } +} + // CreateAPIKey hashes an external API key and broadcasts it to gateways where the API is deployed. // This method is used when external platforms inject API keys to hybrid gateways. func (s *APIKeyService) CreateAPIKey(ctx context.Context, apiHandle, kind, orgId, userId string, req *api.CreateAPIKeyRequest) error { @@ -292,7 +390,9 @@ func (s *APIKeyService) CreateAPIKey(ctx context.Context, apiHandle, kind, orgId } apiId := apiMetadata.ID - // Get all deployments for this API to find target gateways + // Get all deployments for this API to find target gateways. + // An empty list is valid: the key is still persisted centrally and any gateway + // associated later picks it up via deployment-time sync. gateways, err := s.apiRepo.GetAPIGatewaysWithDetails(apiId, orgId) if err != nil { return fmt.Errorf("failed to get API deployments for API handle: %s: %w", apiHandle, err) @@ -358,21 +458,8 @@ func (s *APIKeyService) CreateAPIKey(ctx context.Context, apiHandle, kind, orgId _ = s.auditRepo.Record("CREATE", apiKeyUUID, "api_key", orgId, userId) // Build the API key created event — send the hash JSON and masked key, not the plain key - event := &model.APIKeyCreatedEvent{ - UUID: apiKeyUUID, - ApiId: apiId, - Name: keyName, - ApiKeyHashes: apiKeyHashesJSON, - MaskedApiKey: maskedAPIKey, - ExternalRefId: req.ExternalRefId, - Issuer: issuer, - CreatedAt: dbKey.CreatedAt.Format(time.RFC3339), - UpdatedAt: dbKey.UpdatedAt.Format(time.RFC3339), - } - if expiresAt != nil { - expiresAtStr := expiresAt.Format(time.RFC3339) - event.ExpiresAt = &expiresAtStr - } + event := APIKeyCreatedEventFromModel(dbKey) + event.ExternalRefId = req.ExternalRefId successCount := 0 failureCount := 0 diff --git a/platform-api/internal/service/artifact_dp_apikey_test.go b/platform-api/internal/service/artifact_dp_apikey_test.go index ae0b0f1da..02cda7f72 100644 --- a/platform-api/internal/service/artifact_dp_apikey_test.go +++ b/platform-api/internal/service/artifact_dp_apikey_test.go @@ -50,14 +50,23 @@ func (dpNoopEventHub) UnsubscribeAll(string) error { return func (dpNoopEventHub) CleanUpEvents() error { return nil } func (dpNoopEventHub) Close() error { return nil } -// dpKeyGatewayRepo returns a single gateway for the org so API-key creation has a -// broadcast target (an empty list would short-circuit with ErrGatewayUnavailable). -type dpKeyGatewayRepo struct { - repository.GatewayRepository +// dpKeyAPIRepo returns a single associated gateway so LLM API-key creation has a +// broadcast target. Association-scoped selection mirrors the REST key path. +type dpKeyAPIRepo struct { + repository.APIRepository } -func (dpKeyGatewayRepo) GetByOrganizationID(string) ([]*model.Gateway, error) { - return []*model.Gateway{{ID: "gw-1"}}, nil +func (dpKeyAPIRepo) GetAPIGatewaysWithDetails(string, string) ([]*model.APIGatewayWithDetails, error) { + return []*model.APIGatewayWithDetails{{ID: "gw-1", Name: "gw-1"}}, nil +} + +// dpKeyNoAssocAPIRepo models an artifact with NO gateway associations (undeployed). +type dpKeyNoAssocAPIRepo struct { + repository.APIRepository +} + +func (dpKeyNoAssocAPIRepo) GetAPIGatewaysWithDetails(string, string) ([]*model.APIGatewayWithDetails, error) { + return nil, nil } // dpCapturingAPIKeyRepo captures the persisted API key so tests can assert it was created @@ -91,7 +100,7 @@ func TestLLMProviderAPIKey_AllowedForDPOrigin(t *testing.T) { getByIDFunc: func(string, string) (*model.LLMProvider, error) { return provider, nil }, } keyRepo := &dpCapturingAPIKeyRepo{} - svc := NewLLMProviderAPIKeyService(providerRepo, dpKeyGatewayRepo{}, keyRepo, newDPKeyEventsService(), newTestIdentityService(), newTestLogger()) + svc := NewLLMProviderAPIKeyService(providerRepo, dpKeyAPIRepo{}, keyRepo, newDPKeyEventsService(), newTestIdentityService(), newTestLogger()) resp, err := svc.CreateLLMProviderAPIKey(context.Background(), "dp-prov", "org-1", "", &api.CreateLLMProviderAPIKeyRequest{DisplayName: "dp-consumer-key"}) @@ -124,7 +133,7 @@ func TestLLMProxyAPIKey_AllowedForDPOrigin(t *testing.T) { getByIDFunc: func(string, string) (*model.LLMProxy, error) { return proxy, nil }, } keyRepo := &dpCapturingAPIKeyRepo{} - svc := NewLLMProxyAPIKeyService(proxyRepo, dpKeyGatewayRepo{}, keyRepo, newDPKeyEventsService(), newTestIdentityService(), newTestLogger()) + svc := NewLLMProxyAPIKeyService(proxyRepo, dpKeyAPIRepo{}, keyRepo, newDPKeyEventsService(), newTestIdentityService(), newTestLogger()) resp, err := svc.CreateLLMProxyAPIKey(context.Background(), "dp-proxy", "org-1", "", &api.CreateLLMProxyAPIKeyRequest{DisplayName: "dp-consumer-key"}) @@ -141,3 +150,76 @@ func TestLLMProxyAPIKey_AllowedForDPOrigin(t *testing.T) { t.Errorf("persisted key ArtifactUUID = %q, want proxy UUID %q", keyRepo.created.ArtifactUUID, proxy.UUID) } } + +// TestCreateLLMProviderAPIKey_AssociationScoped verifies that creating a key for a +// provider with NO gateway associations persists the key and broadcasts to ZERO +// gateways — i.e. the broadcast is association-scoped, not org-wide (issue: LLM key +// events were previously sent to every org gateway, causing "artifact not found"). +func TestCreateLLMProviderAPIKey_AssociationScoped(t *testing.T) { + provider := &model.LLMProvider{ + UUID: "prov-uuid", + ID: "prov", + OrganizationUUID: "org-1", + Name: "Prov", + Version: "v1.0", + } + providerRepo := &mockLLMProviderRepo{ + getByIDFunc: func(string, string) (*model.LLMProvider, error) { return provider, nil }, + } + keyRepo := &dpCapturingAPIKeyRepo{} + hub := &capturingEventHub{} + events := NewGatewayEventsService(hub, newTestIdentityService(), newTestLogger()) + + svc := NewLLMProviderAPIKeyService(providerRepo, dpKeyNoAssocAPIRepo{}, keyRepo, + events, newTestIdentityService(), newTestLogger()) + + resp, err := svc.CreateLLMProviderAPIKey(context.Background(), "prov", "org-1", "", + &api.CreateLLMProviderAPIKeyRequest{DisplayName: "k"}) + if err != nil { + t.Fatalf("CreateLLMProviderAPIKey with no associations = %v, want success", err) + } + if resp == nil || resp.ApiKey == "" { + t.Fatalf("expected a generated key, got %#v", resp) + } + if keyRepo.created == nil { + t.Fatalf("key was not persisted") + } + if len(hub.published) != 0 { + t.Fatalf("expected 0 broadcasts for an unassociated provider, got %d", len(hub.published)) + } +} + +// TestCreateLLMProxyAPIKey_AssociationScoped is the LLM-proxy counterpart. +func TestCreateLLMProxyAPIKey_AssociationScoped(t *testing.T) { + proxy := &model.LLMProxy{ + UUID: "proxy-uuid", + ID: "proxy", + OrganizationUUID: "org-1", + Name: "Proxy", + Version: "v1.0", + } + proxyRepo := &mockLLMProxyRepo{ + getByIDFunc: func(string, string) (*model.LLMProxy, error) { return proxy, nil }, + } + keyRepo := &dpCapturingAPIKeyRepo{} + hub := &capturingEventHub{} + events := NewGatewayEventsService(hub, newTestIdentityService(), newTestLogger()) + + svc := NewLLMProxyAPIKeyService(proxyRepo, dpKeyNoAssocAPIRepo{}, keyRepo, + events, newTestIdentityService(), newTestLogger()) + + resp, err := svc.CreateLLMProxyAPIKey(context.Background(), "proxy", "org-1", "", + &api.CreateLLMProxyAPIKeyRequest{DisplayName: "k"}) + if err != nil { + t.Fatalf("CreateLLMProxyAPIKey with no associations = %v, want success", err) + } + if resp == nil || resp.ApiKey == "" { + t.Fatalf("expected a generated key, got %#v", resp) + } + if keyRepo.created == nil { + t.Fatalf("key was not persisted") + } + if len(hub.published) != 0 { + t.Fatalf("expected 0 broadcasts for an unassociated proxy, got %d", len(hub.published)) + } +} diff --git a/platform-api/internal/service/deployment.go b/platform-api/internal/service/deployment.go index e8240f063..d00d8ae9e 100644 --- a/platform-api/internal/service/deployment.go +++ b/platform-api/internal/service/deployment.go @@ -48,6 +48,7 @@ type DeploymentService struct { deploymentRepo repository.DeploymentRepository gatewayRepo repository.GatewayRepository orgRepo repository.OrganizationRepository + apiKeyRepo repository.APIKeyRepository gatewayEventsService *GatewayEventsService auditRepo repository.AuditRepository apiUtil *utils.APIUtil @@ -62,6 +63,7 @@ func NewDeploymentService( deploymentRepo repository.DeploymentRepository, gatewayRepo repository.GatewayRepository, orgRepo repository.OrganizationRepository, + apiKeyRepo repository.APIKeyRepository, gatewayEventsService *GatewayEventsService, auditRepo repository.AuditRepository, apiUtil *utils.APIUtil, @@ -74,6 +76,7 @@ func NewDeploymentService( deploymentRepo: deploymentRepo, gatewayRepo: gatewayRepo, orgRepo: orgRepo, + apiKeyRepo: apiKeyRepo, gatewayEventsService: gatewayEventsService, auditRepo: auditRepo, apiUtil: apiUtil, @@ -342,6 +345,11 @@ func (s *DeploymentService) DeployAPI(apiUUID string, req *api.DeployRequest, or if err := s.gatewayEventsService.BroadcastDeploymentEvent(gatewayID, deploymentEvent); err != nil { s.slogger.Warn("Failed to broadcast deployment event", "error", err) } + + // Push existing active API keys for this artifact to the (possibly newly + // associated) gateway so keys created before this association are recognized + // immediately, rather than only after the controller's next reconnect sync. + s.backfillAPIKeysToGateway(apiUUID, gatewayID, createdBy) } return toAPIDeploymentResponse( @@ -424,6 +432,10 @@ func (s *DeploymentService) RestoreDeployment(apiUUID, deploymentID, gatewayID, if err := s.gatewayEventsService.BroadcastDeploymentEvent(targetDeployment.GatewayID, deploymentEvent); err != nil { s.slogger.Warn("Failed to broadcast deployment event", "error", err) } + + // Push existing active API keys for this artifact to the gateway so a restored + // deployment recognizes pre-existing keys immediately (see backfillAPIKeysToGateway). + s.backfillAPIKeysToGateway(apiUUID, targetDeployment.GatewayID, actor) } if s.auditRepo != nil { @@ -876,6 +888,13 @@ func (s *DeploymentService) ensureAPIGatewayAssociation(apiUUID, gatewayID, orgU return s.apiRepo.CreateAPIAssociation(association) } +// backfillAPIKeysToGateway delegates to the shared BackfillAPIKeysToGateway helper so +// every deploy path (REST, LLM provider/proxy, MCP, WebSub, WebBroker) pushes existing +// keys identically. +func (s *DeploymentService) backfillAPIKeysToGateway(apiUUID, gatewayID, actor string) { + BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayEventsService, s.slogger, apiUUID, gatewayID, actor) +} + // DeployAPIByHandle creates a new immutable deployment artifact using API handle func (s *DeploymentService) DeployAPIByHandle(apiHandle string, req *api.DeployRequest, orgUUID, createdBy string) (*api.DeploymentResponse, error) { // Convert API handle to UUID diff --git a/platform-api/internal/service/deployment_apikey_backfill_test.go b/platform-api/internal/service/deployment_apikey_backfill_test.go new file mode 100644 index 000000000..2a90fcd99 --- /dev/null +++ b/platform-api/internal/service/deployment_apikey_backfill_test.go @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * 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 service + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/wso2/api-platform/common/eventhub" + "github.com/wso2/api-platform/platform-api/internal/constants" + "github.com/wso2/api-platform/platform-api/internal/dto" + "github.com/wso2/api-platform/platform-api/internal/model" + "github.com/wso2/api-platform/platform-api/internal/repository" +) + +// capturingEventHub records every published event so tests can assert what was broadcast. +type capturingEventHub struct { + published []eventhub.Event +} + +func (h *capturingEventHub) Initialize() error { return nil } +func (h *capturingEventHub) RegisterGateway(string) error { return nil } +func (h *capturingEventHub) PublishEvent(_ string, e eventhub.Event) error { + h.published = append(h.published, e) + return nil +} +func (h *capturingEventHub) Subscribe(string) (<-chan eventhub.Event, error) { return nil, nil } +func (h *capturingEventHub) Unsubscribe(string, <-chan eventhub.Event) error { return nil } +func (h *capturingEventHub) UnsubscribeAll(string) error { return nil } +func (h *capturingEventHub) CleanUpEvents() error { return nil } +func (h *capturingEventHub) Close() error { return nil } + +// stubBackfillAPIKeyRepo returns a fixed key list from ListByArtifact. +type stubBackfillAPIKeyRepo struct { + repository.APIKeyRepository + keys []*model.APIKey + err error +} + +func (r *stubBackfillAPIKeyRepo) ListByArtifact(string) ([]*model.APIKey, error) { + return r.keys, r.err +} + +// decodeKeyName extracts the API key name from a captured apikey.created event. +func decodeKeyName(t *testing.T, e eventhub.Event) string { + t.Helper() + var envelope dto.GatewayEventDTO + if err := json.Unmarshal([]byte(e.EventData), &envelope); err != nil { + t.Fatalf("failed to decode event envelope: %v", err) + } + if envelope.Type != EventTypeAPIKeyCreated { + t.Fatalf("unexpected event type %q, want %q", envelope.Type, EventTypeAPIKeyCreated) + } + payloadBytes, err := json.Marshal(envelope.Payload) + if err != nil { + t.Fatalf("failed to re-marshal payload: %v", err) + } + var key model.APIKeyCreatedEvent + if err := json.Unmarshal(payloadBytes, &key); err != nil { + t.Fatalf("failed to decode api key payload: %v", err) + } + return key.Name +} + +func TestBackfillAPIKeysToGateway(t *testing.T) { + past := time.Now().Add(-time.Hour) + future := time.Now().Add(time.Hour) + + keys := []*model.APIKey{ + {UUID: "k-active", ArtifactUUID: "api-1", Name: "active-key", Status: constants.APIKeyStatusActive}, + {UUID: "k-future", ArtifactUUID: "api-1", Name: "active-future-key", Status: constants.APIKeyStatusActive, ExpiresAt: &future}, + {UUID: "k-expired", ArtifactUUID: "api-1", Name: "expired-key", Status: constants.APIKeyStatusActive, ExpiresAt: &past}, + {UUID: "k-revoked", ArtifactUUID: "api-1", Name: "revoked-key", Status: "revoked"}, + nil, + } + + hub := &capturingEventHub{} + svc := &DeploymentService{ + apiKeyRepo: &stubBackfillAPIKeyRepo{keys: keys}, + gatewayEventsService: NewGatewayEventsService(hub, newTestIdentityService(), newTestLogger()), + slogger: newTestLogger(), + } + + svc.backfillAPIKeysToGateway("api-1", "gw-B", "actor-1") + + // Only the two active, non-expired keys should be broadcast. + if len(hub.published) != 2 { + t.Fatalf("expected 2 broadcast events, got %d", len(hub.published)) + } + + got := map[string]bool{} + for _, e := range hub.published { + if e.GatewayID != "gw-B" { + t.Errorf("event broadcast to gateway %q, want gw-B", e.GatewayID) + } + got[decodeKeyName(t, e)] = true + } + for _, want := range []string{"active-key", "active-future-key"} { + if !got[want] { + t.Errorf("expected key %q to be backfilled, but it was not", want) + } + } + for _, notWant := range []string{"expired-key", "revoked-key"} { + if got[notWant] { + t.Errorf("key %q should not have been backfilled", notWant) + } + } +} + +// TestBackfillAPIKeysToGateway_NoKeys ensures an empty artifact broadcasts nothing and does not panic. +func TestBackfillAPIKeysToGateway_NoKeys(t *testing.T) { + hub := &capturingEventHub{} + svc := &DeploymentService{ + apiKeyRepo: &stubBackfillAPIKeyRepo{keys: nil}, + gatewayEventsService: NewGatewayEventsService(hub, newTestIdentityService(), newTestLogger()), + slogger: newTestLogger(), + } + + svc.backfillAPIKeysToGateway("api-1", "gw-B", "actor-1") + + if len(hub.published) != 0 { + t.Fatalf("expected no broadcast events, got %d", len(hub.published)) + } +} + +// TestBackfillAPIKeysToGateway_NilDeps is a no-op when dependencies are unset (e.g. events disabled). +func TestBackfillAPIKeysToGateway_NilDeps(t *testing.T) { + svc := &DeploymentService{slogger: newTestLogger()} + // Must not panic despite nil apiKeyRepo / gatewayEventsService. + svc.backfillAPIKeysToGateway("api-1", "gw-B", "actor-1") +} + +// TestBackfillAPIKeysToGateway_ExportedSharedByAllKinds exercises the exported helper +// directly — the single implementation the REST, LLM provider/proxy, MCP, WebSub and +// WebBroker deploy paths all delegate to. Also asserts nil-slogger safety (some call +// sites may pass a nil logger). +func TestBackfillAPIKeysToGateway_ExportedSharedByAllKinds(t *testing.T) { + future := time.Now().Add(time.Hour) + repo := &stubBackfillAPIKeyRepo{keys: []*model.APIKey{ + {UUID: "k1", ArtifactUUID: "artifact-1", Name: "k1", Status: constants.APIKeyStatusActive}, + {UUID: "k2", ArtifactUUID: "artifact-1", Name: "k2", Status: constants.APIKeyStatusActive, ExpiresAt: &future}, + }} + hub := &capturingEventHub{} + events := NewGatewayEventsService(hub, newTestIdentityService(), newTestLogger()) + + // nil slogger must not panic and must still broadcast. + BackfillAPIKeysToGateway(repo, events, nil, "artifact-1", "gw-X", "") + + if len(hub.published) != 2 { + t.Fatalf("expected 2 broadcast events, got %d", len(hub.published)) + } + for _, e := range hub.published { + if e.GatewayID != "gw-X" { + t.Errorf("event broadcast to %q, want gw-X", e.GatewayID) + } + } +} + +// TestBackfillAPIKeysToGateway_RepoError stops silently (best-effort) when the key +// lookup fails — the deployment must not be blocked by a backfill error. +func TestBackfillAPIKeysToGateway_RepoError(t *testing.T) { + repo := &stubBackfillAPIKeyRepo{err: fmt.Errorf("db down")} + hub := &capturingEventHub{} + events := NewGatewayEventsService(hub, newTestIdentityService(), newTestLogger()) + + BackfillAPIKeysToGateway(repo, events, newTestLogger(), "artifact-1", "gw-X", "") + + if len(hub.published) != 0 { + t.Fatalf("expected no broadcasts on repo error, got %d", len(hub.published)) + } +} diff --git a/platform-api/internal/service/llm_deployment.go b/platform-api/internal/service/llm_deployment.go index b64f2cc96..b41706de4 100644 --- a/platform-api/internal/service/llm_deployment.go +++ b/platform-api/internal/service/llm_deployment.go @@ -57,6 +57,7 @@ type LLMProviderDeploymentService struct { deploymentRepo repository.DeploymentRepository gatewayRepo repository.GatewayRepository orgRepo repository.OrganizationRepository + apiKeyRepo repository.APIKeyRepository gatewayEventsService *GatewayEventsService cfg *config.Server slogger *slog.Logger @@ -69,6 +70,7 @@ type LLMProxyDeploymentService struct { deploymentRepo repository.DeploymentRepository gatewayRepo repository.GatewayRepository orgRepo repository.OrganizationRepository + apiKeyRepo repository.APIKeyRepository gatewayEventsService *GatewayEventsService cfg *config.Server slogger *slog.Logger @@ -81,6 +83,7 @@ func NewLLMProviderDeploymentService( deploymentRepo repository.DeploymentRepository, gatewayRepo repository.GatewayRepository, orgRepo repository.OrganizationRepository, + apiKeyRepo repository.APIKeyRepository, gatewayEventsService *GatewayEventsService, cfg *config.Server, slogger *slog.Logger, @@ -91,6 +94,7 @@ func NewLLMProviderDeploymentService( deploymentRepo: deploymentRepo, gatewayRepo: gatewayRepo, orgRepo: orgRepo, + apiKeyRepo: apiKeyRepo, gatewayEventsService: gatewayEventsService, cfg: cfg, slogger: slogger, @@ -103,6 +107,7 @@ func NewLLMProxyDeploymentService( deploymentRepo repository.DeploymentRepository, gatewayRepo repository.GatewayRepository, orgRepo repository.OrganizationRepository, + apiKeyRepo repository.APIKeyRepository, gatewayEventsService *GatewayEventsService, cfg *config.Server, slogger *slog.Logger, @@ -112,6 +117,7 @@ func NewLLMProxyDeploymentService( deploymentRepo: deploymentRepo, gatewayRepo: gatewayRepo, orgRepo: orgRepo, + apiKeyRepo: apiKeyRepo, gatewayEventsService: gatewayEventsService, cfg: cfg, slogger: slogger, @@ -277,6 +283,11 @@ func (s *LLMProviderDeploymentService) DeployLLMProvider(providerID string, req if err := s.gatewayEventsService.BroadcastLLMProviderDeploymentEvent(gatewayID, deploymentEvent); err != nil { s.slogger.Warn("Failed to broadcast LLM provider deployment event", "error", err) } + + // Push existing active API keys for this provider to the (possibly newly + // associated) gateway so keys created before this association are recognized + // immediately, rather than only after the controller's next reconnect sync. + BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayEventsService, s.slogger, provider.UUID, gatewayID, "") } return toAPIDeploymentResponse( @@ -369,6 +380,9 @@ func (s *LLMProviderDeploymentService) RestoreLLMProviderDeployment(providerID, if err := s.gatewayEventsService.BroadcastLLMProviderDeploymentEvent(targetDeployment.GatewayID, deploymentEvent); err != nil { s.slogger.Warn("Failed to broadcast LLM provider deployment event", "error", err) } + + // Backfill existing active API keys to the gateway (see BackfillAPIKeysToGateway). + BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayEventsService, s.slogger, provider.UUID, targetDeployment.GatewayID, "") } return toAPIDeploymentResponse( @@ -1441,6 +1455,9 @@ func (s *LLMProxyDeploymentService) DeployLLMProxy(proxyID string, req *api.Depl if err := s.gatewayEventsService.BroadcastLLMProxyDeploymentEvent(gatewayID, deploymentEvent); err != nil { s.slogger.Warn("Failed to broadcast LLM proxy deployment event", "error", err) } + + // Push existing active API keys for this proxy to the gateway (see BackfillAPIKeysToGateway). + BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayEventsService, s.slogger, proxy.UUID, gatewayID, "") } return toAPIDeploymentResponse( @@ -1533,6 +1550,9 @@ func (s *LLMProxyDeploymentService) RestoreLLMProxyDeployment(proxyID, deploymen if err := s.gatewayEventsService.BroadcastLLMProxyDeploymentEvent(targetDeployment.GatewayID, deploymentEvent); err != nil { s.slogger.Warn("Failed to broadcast LLM proxy deployment event", "error", err) } + + // Backfill existing active API keys to the gateway (see BackfillAPIKeysToGateway). + BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayEventsService, s.slogger, proxy.UUID, targetDeployment.GatewayID, "") } return toAPIDeploymentResponse( diff --git a/platform-api/internal/service/mcp_deployment.go b/platform-api/internal/service/mcp_deployment.go index a955cd8c6..6405db3a3 100644 --- a/platform-api/internal/service/mcp_deployment.go +++ b/platform-api/internal/service/mcp_deployment.go @@ -42,6 +42,7 @@ type MCPDeploymentService struct { deploymentRepo repository.DeploymentRepository gatewayRepo repository.GatewayRepository orgRepo repository.OrganizationRepository + apiKeyRepo repository.APIKeyRepository gatewayEventsService *GatewayEventsService cfg *config.Server utils *utils.MCPUtils @@ -50,13 +51,14 @@ type MCPDeploymentService struct { func NewMCPDeploymentService(mcpRepo repository.MCPProxyRepository, deploymentRepo repository.DeploymentRepository, gatewayRepo repository.GatewayRepository, orgRepo repository.OrganizationRepository, artifactRepo repository.ArtifactRepository, - gatewayEventsService *GatewayEventsService, cfg *config.Server, slogger *slog.Logger) *MCPDeploymentService { + apiKeyRepo repository.APIKeyRepository, gatewayEventsService *GatewayEventsService, cfg *config.Server, slogger *slog.Logger) *MCPDeploymentService { return &MCPDeploymentService{ mcpRepo: mcpRepo, deploymentRepo: deploymentRepo, gatewayRepo: gatewayRepo, orgRepo: orgRepo, artifactRepo: artifactRepo, + apiKeyRepo: apiKeyRepo, gatewayEventsService: gatewayEventsService, cfg: cfg, utils: &utils.MCPUtils{}, @@ -344,6 +346,9 @@ func (s *MCPDeploymentService) deployMCPProxy(proxyUUID string, req *api.DeployR if err := s.gatewayEventsService.BroadcastMCPProxyDeploymentEvent(gatewayID, deploymentEvent); err != nil { s.slogger.Warn("Failed to broadcast MCP proxy deployment event", "error", err) } + + // Push existing active API keys for this proxy to the gateway (see BackfillAPIKeysToGateway). + BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayEventsService, s.slogger, proxyUUID, gatewayID, createdBy) } // Return deployment response @@ -550,6 +555,9 @@ func (s *MCPDeploymentService) restoreMCPProxyDeployment(proxyUUID string, deplo if err := s.gatewayEventsService.BroadcastMCPProxyDeploymentEvent(targetDeployment.GatewayID, deploymentEvent); err != nil { s.slogger.Warn("Failed to broadcast MCP proxy deployment event", "error", err) } + + // Backfill existing active API keys to the gateway (see BackfillAPIKeysToGateway). + BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayEventsService, s.slogger, proxyUUID, targetDeployment.GatewayID, "") } return toAPIDeploymentResponse( diff --git a/platform-api/plugins/eventgateway/plugin.go b/platform-api/plugins/eventgateway/plugin.go index 95e057e27..68296a1fd 100644 --- a/platform-api/plugins/eventgateway/plugin.go +++ b/platform-api/plugins/eventgateway/plugin.go @@ -153,6 +153,7 @@ func (p *EventGatewayPlugin) Init(deps *plugin.Deps) error { deps.OrgRepo, deps.ArtifactRepo, deps.APIRepo, + deps.APIKeyRepo, deps.GatewayEventsService, cfg, logger, @@ -177,6 +178,7 @@ func (p *EventGatewayPlugin) Init(deps *plugin.Deps) error { deps.OrgRepo, deps.ArtifactRepo, deps.APIRepo, + deps.APIKeyRepo, deps.GatewayEventsService, cfg, logger, diff --git a/platform-api/plugins/eventgateway/service/webbroker_api_deployment.go b/platform-api/plugins/eventgateway/service/webbroker_api_deployment.go index 93661be63..30b701467 100644 --- a/platform-api/plugins/eventgateway/service/webbroker_api_deployment.go +++ b/platform-api/plugins/eventgateway/service/webbroker_api_deployment.go @@ -45,6 +45,7 @@ type WebBrokerAPIDeploymentService struct { deploymentRepo repository.DeploymentRepository gatewayRepo repository.GatewayRepository orgRepo repository.OrganizationRepository + apiKeyRepo repository.APIKeyRepository gatewayEventsService *coreservice.GatewayEventsService cfg *config.Server slogger *slog.Logger @@ -58,6 +59,7 @@ func NewWebBrokerAPIDeploymentService( orgRepo repository.OrganizationRepository, artifactRepo repository.ArtifactRepository, apiRepo repository.APIRepository, + apiKeyRepo repository.APIKeyRepository, gatewayEventsService *coreservice.GatewayEventsService, cfg *config.Server, slogger *slog.Logger, @@ -69,6 +71,7 @@ func NewWebBrokerAPIDeploymentService( orgRepo: orgRepo, artifactRepo: artifactRepo, apiRepo: apiRepo, + apiKeyRepo: apiKeyRepo, gatewayEventsService: gatewayEventsService, cfg: cfg, slogger: slogger, @@ -260,6 +263,9 @@ func (s *WebBrokerAPIDeploymentService) deployWebBrokerAPI(apiUUID string, req * if err := s.gatewayEventsService.BroadcastWebBrokerAPIDeploymentEvent(gatewayID, deploymentEvent); err != nil { s.slogger.Warn("Failed to broadcast WebBroker API deployment event", "error", err) } + + // Push existing active API keys for this API to the gateway (see BackfillAPIKeysToGateway). + coreservice.BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayEventsService, s.slogger, apiUUID, gatewayID, createdBy) } return toAPIDeploymentResponse( @@ -443,6 +449,9 @@ func (s *WebBrokerAPIDeploymentService) restoreWebBrokerAPIDeployment(apiUUID st if err := s.gatewayEventsService.BroadcastWebBrokerAPIDeploymentEvent(targetDeployment.GatewayID, deploymentEvent); err != nil { s.slogger.Warn("Failed to broadcast WebBroker API deployment event", "error", err) } + + // Backfill existing active API keys to the gateway (see BackfillAPIKeysToGateway). + coreservice.BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayEventsService, s.slogger, apiUUID, targetDeployment.GatewayID, "") } return toAPIDeploymentResponse( diff --git a/platform-api/plugins/eventgateway/service/websub_api_deployment.go b/platform-api/plugins/eventgateway/service/websub_api_deployment.go index e81e4a198..ba782ab58 100644 --- a/platform-api/plugins/eventgateway/service/websub_api_deployment.go +++ b/platform-api/plugins/eventgateway/service/websub_api_deployment.go @@ -45,6 +45,7 @@ type WebSubAPIDeploymentService struct { deploymentRepo repository.DeploymentRepository gatewayRepo repository.GatewayRepository orgRepo repository.OrganizationRepository + apiKeyRepo repository.APIKeyRepository gatewayEventsService *coreservice.GatewayEventsService cfg *config.Server slogger *slog.Logger @@ -58,6 +59,7 @@ func NewWebSubAPIDeploymentService( orgRepo repository.OrganizationRepository, artifactRepo repository.ArtifactRepository, apiRepo repository.APIRepository, + apiKeyRepo repository.APIKeyRepository, gatewayEventsService *coreservice.GatewayEventsService, cfg *config.Server, slogger *slog.Logger, @@ -69,6 +71,7 @@ func NewWebSubAPIDeploymentService( orgRepo: orgRepo, artifactRepo: artifactRepo, apiRepo: apiRepo, + apiKeyRepo: apiKeyRepo, gatewayEventsService: gatewayEventsService, cfg: cfg, slogger: slogger, @@ -260,6 +263,9 @@ func (s *WebSubAPIDeploymentService) deployWebSubAPI(apiUUID string, req *api.De if err := s.gatewayEventsService.BroadcastWebSubAPIDeploymentEvent(gatewayID, deploymentEvent); err != nil { s.slogger.Warn("Failed to broadcast WebSub API deployment event", "error", err) } + + // Push existing active API keys for this API to the gateway (see BackfillAPIKeysToGateway). + coreservice.BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayEventsService, s.slogger, apiUUID, gatewayID, createdBy) } return toAPIDeploymentResponse( @@ -441,6 +447,9 @@ func (s *WebSubAPIDeploymentService) restoreWebSubAPIDeployment(apiUUID string, if err := s.gatewayEventsService.BroadcastWebSubAPIDeploymentEvent(targetDeployment.GatewayID, deploymentEvent); err != nil { s.slogger.Warn("Failed to broadcast WebSub API deployment event", "error", err) } + + // Backfill existing active API keys to the gateway (see BackfillAPIKeysToGateway). + coreservice.BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayEventsService, s.slogger, apiUUID, targetDeployment.GatewayID, "") } return toAPIDeploymentResponse( From 6aad35de0097588ba8e9d27ef6127aaa00a6a0eb Mon Sep 17 00:00:00 2001 From: Dinith Herath Date: Fri, 10 Jul 2026 18:45:56 +0530 Subject: [PATCH 3/5] Fix LLM provider/proxy API key events are broadcast to all organization gateways on key creation --- platform-api/internal/service/llm_apikey.go | 31 ++++++++++++------- .../internal/service/llm_proxy_apikey.go | 31 ++++++++++++------- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/platform-api/internal/service/llm_apikey.go b/platform-api/internal/service/llm_apikey.go index 7732cbf1d..364266987 100644 --- a/platform-api/internal/service/llm_apikey.go +++ b/platform-api/internal/service/llm_apikey.go @@ -35,7 +35,7 @@ import ( // LLMProviderAPIKeyService handles API key management for LLM providers type LLMProviderAPIKeyService struct { llmProviderRepo repository.LLMProviderRepository - gatewayRepo repository.GatewayRepository + apiRepo repository.APIRepository apiKeyRepo repository.APIKeyRepository gatewayEventsService *GatewayEventsService identity *IdentityService @@ -45,7 +45,7 @@ type LLMProviderAPIKeyService struct { // NewLLMProviderAPIKeyService creates a new LLM provider API key service instance func NewLLMProviderAPIKeyService( llmProviderRepo repository.LLMProviderRepository, - gatewayRepo repository.GatewayRepository, + apiRepo repository.APIRepository, apiKeyRepo repository.APIKeyRepository, gatewayEventsService *GatewayEventsService, identity *IdentityService, @@ -53,7 +53,7 @@ func NewLLMProviderAPIKeyService( ) *LLMProviderAPIKeyService { return &LLMProviderAPIKeyService{ llmProviderRepo: llmProviderRepo, - gatewayRepo: gatewayRepo, + apiRepo: apiRepo, apiKeyRepo: apiKeyRepo, gatewayEventsService: gatewayEventsService, identity: identity, @@ -163,14 +163,14 @@ func (s *LLMProviderAPIKeyService) DeleteLLMProviderAPIKey( s.slogger.Info("Successfully deleted LLM provider API key", "providerId", providerID, "keyName", keyName) - // Broadcast revoke event to gateways. - gateways, err := s.gatewayRepo.GetByOrganizationID(orgID) + // Broadcast revoke only to gateways the provider is associated with (not all org gateways). + gateways, err := s.apiRepo.GetAPIGatewaysWithDetails(provider.UUID, orgID) if err != nil { - s.slogger.Error("Failed to get gateways for API key revoke broadcast", "providerId", providerID, "keyName", keyName, "error", err) + s.slogger.Error("Failed to get associated gateways for API key revoke broadcast", "providerId", providerID, "keyName", keyName, "error", err) return nil } if len(gateways) == 0 { - s.slogger.Warn("No gateways found for organization; skipping revoke broadcast", "organizationId", orgID) + s.slogger.Info("Provider not associated with any gateway; skipping revoke broadcast", "providerId", providerID, "keyName", keyName) return nil } @@ -179,7 +179,7 @@ func (s *LLMProviderAPIKeyService) DeleteLLMProviderAPIKey( KeyName: keyName, } - for _, gateway := range filterGatewaysByAllowedTargets(gateways, existingKey.AllowedTargets) { + for _, gateway := range filterAPIGatewaysByAllowedTargets(gateways, existingKey.AllowedTargets) { gatewayID := gateway.ID if err := s.gatewayEventsService.BroadcastAPIKeyRevokedEvent(gatewayID, userID, event); err != nil { s.slogger.Error("Failed to broadcast LLM provider API key revoked event", "providerId", providerID, "gatewayId", gatewayID, "keyName", keyName, "error", err) @@ -240,7 +240,10 @@ func (s *LLMProviderAPIKeyService) CreateLLMProviderAPIKey( expiresAt = &expiresAtStr } - gateways, err := s.gatewayRepo.GetByOrganizationID(orgID) + // Broadcast only to gateways the provider is associated with (not all org gateways), + // mirroring the REST key path. An empty list is valid: the key is still persisted and + // any gateway associated later picks it up via the deploy-time backfill. + gateways, err := s.apiRepo.GetAPIGatewaysWithDetails(provider.UUID, orgID) if err != nil { s.slogger.Error("Failed to get gateways for API key broadcast", "providerId", providerID, "error", err) return nil, fmt.Errorf("failed to get gateways: %w", err) @@ -306,7 +309,7 @@ func (s *LLMProviderAPIKeyService) CreateLLMProviderAPIKey( UpdatedAt: dbKey.UpdatedAt.Format(time.RFC3339), } - targetGateways := filterGatewaysByAllowedTargets(gateways, allowedTargets) + targetGateways := filterAPIGatewaysByAllowedTargets(gateways, allowedTargets) successCount := 0 failureCount := 0 var lastError error @@ -329,8 +332,12 @@ func (s *LLMProviderAPIKeyService) CreateLLMProviderAPIKey( s.slogger.Info("LLM provider API key creation broadcast summary", "providerId", providerID, "keyName", name, "total", len(targetGateways), "success", successCount, "failed", failureCount) - if successCount == 0 { - s.slogger.Warn("Failed to deliver LLM provider API key to any gateway; key is saved to the database", "providerId", providerID, "keyName", name, "error", lastError) + if len(targetGateways) == 0 { + // No gateways associated yet — a valid state. The key is persisted centrally and any + // gateway associated later picks it up via the deploy-time backfill. + s.slogger.Info("LLM provider not associated with any gateway; API key saved and will be delivered at deploy time", "providerId", providerID, "keyName", name) + } else if successCount == 0 { + s.slogger.Error("LLM provider API key created event was not broadcast to any associated gateway", "providerId", providerID, "keyName", name, "error", lastError) } return &api.CreateLLMProviderAPIKeyResponse{ diff --git a/platform-api/internal/service/llm_proxy_apikey.go b/platform-api/internal/service/llm_proxy_apikey.go index 17e9d6663..4181dc041 100644 --- a/platform-api/internal/service/llm_proxy_apikey.go +++ b/platform-api/internal/service/llm_proxy_apikey.go @@ -35,7 +35,7 @@ import ( // LLMProxyAPIKeyService handles API key management for LLM proxies type LLMProxyAPIKeyService struct { llmProxyRepo repository.LLMProxyRepository - gatewayRepo repository.GatewayRepository + apiRepo repository.APIRepository apiKeyRepo repository.APIKeyRepository gatewayEventsService *GatewayEventsService identity *IdentityService @@ -45,7 +45,7 @@ type LLMProxyAPIKeyService struct { // NewLLMProxyAPIKeyService creates a new LLM proxy API key service instance func NewLLMProxyAPIKeyService( llmProxyRepo repository.LLMProxyRepository, - gatewayRepo repository.GatewayRepository, + apiRepo repository.APIRepository, apiKeyRepo repository.APIKeyRepository, gatewayEventsService *GatewayEventsService, identity *IdentityService, @@ -53,7 +53,7 @@ func NewLLMProxyAPIKeyService( ) *LLMProxyAPIKeyService { return &LLMProxyAPIKeyService{ llmProxyRepo: llmProxyRepo, - gatewayRepo: gatewayRepo, + apiRepo: apiRepo, apiKeyRepo: apiKeyRepo, gatewayEventsService: gatewayEventsService, identity: identity, @@ -136,14 +136,14 @@ func (s *LLMProxyAPIKeyService) DeleteLLMProxyAPIKey( s.slogger.Info("Successfully deleted LLM proxy API key", "proxyId", proxyID, "keyName", keyName) - // Broadcast revoke event to gateways. - gateways, err := s.gatewayRepo.GetByOrganizationID(orgID) + // Broadcast revoke only to gateways the proxy is associated with (not all org gateways). + gateways, err := s.apiRepo.GetAPIGatewaysWithDetails(proxy.UUID, orgID) if err != nil { - s.slogger.Error("Failed to get gateways for API key revoke broadcast", "proxyId", proxyID, "keyName", keyName, "error", err) + s.slogger.Error("Failed to get associated gateways for API key revoke broadcast", "proxyId", proxyID, "keyName", keyName, "error", err) return nil } if len(gateways) == 0 { - s.slogger.Warn("No gateways found for organization; skipping revoke broadcast", "organizationId", orgID) + s.slogger.Info("Proxy not associated with any gateway; skipping revoke broadcast", "proxyId", proxyID, "keyName", keyName) return nil } @@ -152,7 +152,7 @@ func (s *LLMProxyAPIKeyService) DeleteLLMProxyAPIKey( KeyName: keyName, } - for _, gateway := range filterGatewaysByAllowedTargets(gateways, existingKey.AllowedTargets) { + for _, gateway := range filterAPIGatewaysByAllowedTargets(gateways, existingKey.AllowedTargets) { gatewayID := gateway.ID if err := s.gatewayEventsService.BroadcastAPIKeyRevokedEvent(gatewayID, userID, event); err != nil { s.slogger.Error("Failed to broadcast LLM proxy API key revoked event", "proxyId", proxyID, "gatewayId", gatewayID, "keyName", keyName, "error", err) @@ -203,7 +203,10 @@ func (s *LLMProxyAPIKeyService) CreateLLMProxyAPIKey( displayName = name } - gateways, err := s.gatewayRepo.GetByOrganizationID(orgID) + // Broadcast only to gateways the proxy is associated with (not all org gateways), + // mirroring the REST key path. An empty list is valid: the key is still persisted and + // any gateway associated later picks it up via the deploy-time backfill. + gateways, err := s.apiRepo.GetAPIGatewaysWithDetails(proxy.UUID, orgID) if err != nil { s.slogger.Error("Failed to get gateways for API key broadcast", "proxyId", proxyID, "error", err) return nil, fmt.Errorf("failed to get gateways: %w", err) @@ -275,7 +278,7 @@ func (s *LLMProxyAPIKeyService) CreateLLMProxyAPIKey( UpdatedAt: dbKey.UpdatedAt.Format(time.RFC3339), } - targetGateways := filterGatewaysByAllowedTargets(gateways, allowedTargets) + targetGateways := filterAPIGatewaysByAllowedTargets(gateways, allowedTargets) successCount := 0 failureCount := 0 var lastError error @@ -298,8 +301,12 @@ func (s *LLMProxyAPIKeyService) CreateLLMProxyAPIKey( s.slogger.Info("LLM proxy API key creation broadcast summary", "proxyId", proxyID, "keyName", name, "total", len(targetGateways), "success", successCount, "failed", failureCount) - if successCount == 0 { - s.slogger.Warn("Failed to deliver LLM proxy API key to any gateway; key is saved to the database", "proxyId", proxyID, "keyName", name, "error", lastError) + if len(targetGateways) == 0 { + // No gateways associated yet — a valid state. The key is persisted centrally and any + // gateway associated later picks it up via the deploy-time backfill. + s.slogger.Info("LLM proxy not associated with any gateway; API key saved and will be delivered at deploy time", "proxyId", proxyID, "keyName", name) + } else if successCount == 0 { + s.slogger.Error("LLM proxy API key created event was not broadcast to any associated gateway", "proxyId", proxyID, "keyName", name, "error", lastError) } return &api.CreateLLMProxyAPIKeyResponse{ From 5951ae0821d64393481168c51611ef52cc91e7d7 Mon Sep 17 00:00:00 2001 From: Dinith Herath Date: Fri, 10 Jul 2026 19:51:29 +0530 Subject: [PATCH 4/5] Enfore allowed targets in backfilling API keys for artifacts --- platform-api/internal/service/apikey.go | 50 +++++++++++++++---- platform-api/internal/service/deployment.go | 2 +- .../deployment_apikey_backfill_test.go | 46 ++++++++++++++++- .../internal/service/llm_deployment.go | 8 +-- .../internal/service/mcp_deployment.go | 4 +- .../service/webbroker_api_deployment.go | 4 +- .../service/websub_api_deployment.go | 4 +- 7 files changed, 94 insertions(+), 24 deletions(-) diff --git a/platform-api/internal/service/apikey.go b/platform-api/internal/service/apikey.go index 0e3f1b210..2d1af9604 100644 --- a/platform-api/internal/service/apikey.go +++ b/platform-api/internal/service/apikey.go @@ -166,13 +166,9 @@ func filterGatewaysByAllowedTargets(gateways []*model.Gateway, allowedTargets st if allowedTargets == "" || allowedTargets == constants.APIKeyAllowedTargetsAll { return gateways } - allowed := make(map[string]struct{}) - for _, name := range strings.Split(allowedTargets, ",") { - allowed[strings.TrimSpace(name)] = struct{}{} - } filtered := make([]*model.Gateway, 0, len(gateways)) for _, gw := range gateways { - if _, ok := allowed[gw.Name]; ok { + if gatewayAllowedByTargets(gw.Name, allowedTargets) { filtered = append(filtered, gw) } } @@ -186,19 +182,31 @@ func filterAPIGatewaysByAllowedTargets(gateways []*model.APIGatewayWithDetails, if allowedTargets == "" || allowedTargets == constants.APIKeyAllowedTargetsAll { return gateways } - allowed := make(map[string]struct{}) - for _, name := range strings.Split(allowedTargets, ",") { - allowed[strings.TrimSpace(name)] = struct{}{} - } filtered := make([]*model.APIGatewayWithDetails, 0, len(gateways)) for _, gw := range gateways { - if _, ok := allowed[gw.Name]; ok { + if gatewayAllowedByTargets(gw.Name, allowedTargets) { filtered = append(filtered, gw) } } return filtered } +// gatewayAllowedByTargets reports whether a gateway (identified by name) is permitted by a +// key's allowedTargets. An empty value or constants.APIKeyAllowedTargetsAll ("ALL") permits +// every gateway; otherwise the gateway name must appear in the comma-separated list. This is +// the single source of truth shared by the filter helpers and the deploy-time backfill. +func gatewayAllowedByTargets(gatewayName, allowedTargets string) bool { + if allowedTargets == "" || allowedTargets == constants.APIKeyAllowedTargetsAll { + return true + } + for _, name := range strings.Split(allowedTargets, ",") { + if strings.TrimSpace(name) == gatewayName { + return true + } + } + return false +} + // randomHexString generates a random lowercase hex string of the requested length. func randomHexString(n int) (string, error) { bytes := make([]byte, (n+1)/2) @@ -332,7 +340,7 @@ func APIKeyCreatedEventFromModel(k *model.APIKey) *model.APIKeyCreatedEvent { // Best-effort: the controller upserts apikey.created idempotently, so re-sending a key the // gateway already holds is harmless, and any failure is logged without blocking the // deployment (the reconnect bulk sync remains the safety net). -func BackfillAPIKeysToGateway(apiKeyRepo repository.APIKeyRepository, events *GatewayEventsService, slogger *slog.Logger, artifactUUID, gatewayID, actor string) { +func BackfillAPIKeysToGateway(apiKeyRepo repository.APIKeyRepository, gatewayRepo repository.GatewayRepository, events *GatewayEventsService, slogger *slog.Logger, artifactUUID, gatewayID, actor string) { if events == nil || apiKeyRepo == nil { return } @@ -346,6 +354,22 @@ func BackfillAPIKeysToGateway(apiKeyRepo repository.APIKeyRepository, events *Ga return } + // Resolve the target gateway's name once so we can honor each key's AllowedTargets, + // which is matched by gateway name — consistent with the create/revoke broadcast paths + // (filterGatewaysByAllowedTargets). If the name can't be resolved, keys restricted to + // specific targets are conservatively skipped (only "ALL"/unrestricted keys go through). + gatewayName := "" + if gatewayRepo != nil { + if gw, gwErr := gatewayRepo.GetByUUID(gatewayID); gwErr != nil { + if slogger != nil { + slogger.Warn("Failed to resolve gateway for API key backfill target filtering", + "gatewayId", gatewayID, "error", gwErr) + } + } else if gw != nil { + gatewayName = gw.Name + } + } + now := time.Now() backfilled := 0 for _, k := range keys { @@ -356,6 +380,10 @@ func BackfillAPIKeysToGateway(apiKeyRepo repository.APIKeyRepository, events *Ga if k.ExpiresAt != nil && !k.ExpiresAt.After(now) { continue } + // Honor per-key AllowedTargets — skip keys not permitted for this gateway. + if !gatewayAllowedByTargets(gatewayName, k.AllowedTargets) { + continue + } event := APIKeyCreatedEventFromModel(k) if err := events.BroadcastAPIKeyCreatedEvent(gatewayID, actor, event); err != nil { diff --git a/platform-api/internal/service/deployment.go b/platform-api/internal/service/deployment.go index d00d8ae9e..6879faacd 100644 --- a/platform-api/internal/service/deployment.go +++ b/platform-api/internal/service/deployment.go @@ -892,7 +892,7 @@ func (s *DeploymentService) ensureAPIGatewayAssociation(apiUUID, gatewayID, orgU // every deploy path (REST, LLM provider/proxy, MCP, WebSub, WebBroker) pushes existing // keys identically. func (s *DeploymentService) backfillAPIKeysToGateway(apiUUID, gatewayID, actor string) { - BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayEventsService, s.slogger, apiUUID, gatewayID, actor) + BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayRepo, s.gatewayEventsService, s.slogger, apiUUID, gatewayID, actor) } // DeployAPIByHandle creates a new immutable deployment artifact using API handle diff --git a/platform-api/internal/service/deployment_apikey_backfill_test.go b/platform-api/internal/service/deployment_apikey_backfill_test.go index 2a90fcd99..f9cfd92d5 100644 --- a/platform-api/internal/service/deployment_apikey_backfill_test.go +++ b/platform-api/internal/service/deployment_apikey_backfill_test.go @@ -58,6 +58,17 @@ func (r *stubBackfillAPIKeyRepo) ListByArtifact(string) ([]*model.APIKey, error) return r.keys, r.err } +// stubBackfillGatewayRepo resolves a gateway UUID to a gateway with a fixed name so the +// backfill can evaluate per-key AllowedTargets (which match by gateway name). +type stubBackfillGatewayRepo struct { + repository.GatewayRepository + name string +} + +func (r *stubBackfillGatewayRepo) GetByUUID(id string) (*model.Gateway, error) { + return &model.Gateway{ID: id, Name: r.name}, nil +} + // decodeKeyName extracts the API key name from a captured apikey.created event. func decodeKeyName(t *testing.T, e eventhub.Event) string { t.Helper() @@ -161,7 +172,7 @@ func TestBackfillAPIKeysToGateway_ExportedSharedByAllKinds(t *testing.T) { events := NewGatewayEventsService(hub, newTestIdentityService(), newTestLogger()) // nil slogger must not panic and must still broadcast. - BackfillAPIKeysToGateway(repo, events, nil, "artifact-1", "gw-X", "") + BackfillAPIKeysToGateway(repo, nil, events, nil, "artifact-1", "gw-X", "") if len(hub.published) != 2 { t.Fatalf("expected 2 broadcast events, got %d", len(hub.published)) @@ -180,9 +191,40 @@ func TestBackfillAPIKeysToGateway_RepoError(t *testing.T) { hub := &capturingEventHub{} events := NewGatewayEventsService(hub, newTestIdentityService(), newTestLogger()) - BackfillAPIKeysToGateway(repo, events, newTestLogger(), "artifact-1", "gw-X", "") + BackfillAPIKeysToGateway(repo, nil, events, newTestLogger(), "artifact-1", "gw-X", "") if len(hub.published) != 0 { t.Fatalf("expected no broadcasts on repo error, got %d", len(hub.published)) } } + +// TestBackfillAPIKeysToGateway_HonorsAllowedTargets verifies a key is backfilled only to a +// gateway permitted by its AllowedTargets — matching the create/revoke broadcast behaviour. +func TestBackfillAPIKeysToGateway_HonorsAllowedTargets(t *testing.T) { + keys := []*model.APIKey{ + {UUID: "k-all", ArtifactUUID: "api-1", Name: "all-key", Status: constants.APIKeyStatusActive, AllowedTargets: constants.APIKeyAllowedTargetsAll}, + {UUID: "k-here", ArtifactUUID: "api-1", Name: "here-key", Status: constants.APIKeyStatusActive, AllowedTargets: "gw-target,other-gw"}, + {UUID: "k-else", ArtifactUUID: "api-1", Name: "elsewhere-key", Status: constants.APIKeyStatusActive, AllowedTargets: "other-gw"}, + } + hub := &capturingEventHub{} + events := NewGatewayEventsService(hub, newTestIdentityService(), newTestLogger()) + + // Target gateway "gw-target": the ALL key and the key that lists gw-target should go through; + // the key restricted to "other-gw" must be skipped. + BackfillAPIKeysToGateway(&stubBackfillAPIKeyRepo{keys: keys}, &stubBackfillGatewayRepo{name: "gw-target"}, + events, newTestLogger(), "api-1", "gw-uuid", "") + + if len(hub.published) != 2 { + t.Fatalf("expected 2 broadcasts (ALL + gw-target-listed), got %d", len(hub.published)) + } + got := map[string]bool{} + for _, e := range hub.published { + got[decodeKeyName(t, e)] = true + } + if !got["all-key"] || !got["here-key"] { + t.Errorf("expected all-key and here-key backfilled; got %v", got) + } + if got["elsewhere-key"] { + t.Errorf("key restricted to other-gw must not be backfilled to gw-target") + } +} diff --git a/platform-api/internal/service/llm_deployment.go b/platform-api/internal/service/llm_deployment.go index b41706de4..4bc1a8147 100644 --- a/platform-api/internal/service/llm_deployment.go +++ b/platform-api/internal/service/llm_deployment.go @@ -287,7 +287,7 @@ func (s *LLMProviderDeploymentService) DeployLLMProvider(providerID string, req // Push existing active API keys for this provider to the (possibly newly // associated) gateway so keys created before this association are recognized // immediately, rather than only after the controller's next reconnect sync. - BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayEventsService, s.slogger, provider.UUID, gatewayID, "") + BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayRepo, s.gatewayEventsService, s.slogger, provider.UUID, gatewayID, "") } return toAPIDeploymentResponse( @@ -382,7 +382,7 @@ func (s *LLMProviderDeploymentService) RestoreLLMProviderDeployment(providerID, } // Backfill existing active API keys to the gateway (see BackfillAPIKeysToGateway). - BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayEventsService, s.slogger, provider.UUID, targetDeployment.GatewayID, "") + BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayRepo, s.gatewayEventsService, s.slogger, provider.UUID, targetDeployment.GatewayID, "") } return toAPIDeploymentResponse( @@ -1457,7 +1457,7 @@ func (s *LLMProxyDeploymentService) DeployLLMProxy(proxyID string, req *api.Depl } // Push existing active API keys for this proxy to the gateway (see BackfillAPIKeysToGateway). - BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayEventsService, s.slogger, proxy.UUID, gatewayID, "") + BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayRepo, s.gatewayEventsService, s.slogger, proxy.UUID, gatewayID, "") } return toAPIDeploymentResponse( @@ -1552,7 +1552,7 @@ func (s *LLMProxyDeploymentService) RestoreLLMProxyDeployment(proxyID, deploymen } // Backfill existing active API keys to the gateway (see BackfillAPIKeysToGateway). - BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayEventsService, s.slogger, proxy.UUID, targetDeployment.GatewayID, "") + BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayRepo, s.gatewayEventsService, s.slogger, proxy.UUID, targetDeployment.GatewayID, "") } return toAPIDeploymentResponse( diff --git a/platform-api/internal/service/mcp_deployment.go b/platform-api/internal/service/mcp_deployment.go index 6405db3a3..64c3c0512 100644 --- a/platform-api/internal/service/mcp_deployment.go +++ b/platform-api/internal/service/mcp_deployment.go @@ -348,7 +348,7 @@ func (s *MCPDeploymentService) deployMCPProxy(proxyUUID string, req *api.DeployR } // Push existing active API keys for this proxy to the gateway (see BackfillAPIKeysToGateway). - BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayEventsService, s.slogger, proxyUUID, gatewayID, createdBy) + BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayRepo, s.gatewayEventsService, s.slogger, proxyUUID, gatewayID, createdBy) } // Return deployment response @@ -557,7 +557,7 @@ func (s *MCPDeploymentService) restoreMCPProxyDeployment(proxyUUID string, deplo } // Backfill existing active API keys to the gateway (see BackfillAPIKeysToGateway). - BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayEventsService, s.slogger, proxyUUID, targetDeployment.GatewayID, "") + BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayRepo, s.gatewayEventsService, s.slogger, proxyUUID, targetDeployment.GatewayID, "") } return toAPIDeploymentResponse( diff --git a/platform-api/plugins/eventgateway/service/webbroker_api_deployment.go b/platform-api/plugins/eventgateway/service/webbroker_api_deployment.go index 30b701467..dcd8b1973 100644 --- a/platform-api/plugins/eventgateway/service/webbroker_api_deployment.go +++ b/platform-api/plugins/eventgateway/service/webbroker_api_deployment.go @@ -265,7 +265,7 @@ func (s *WebBrokerAPIDeploymentService) deployWebBrokerAPI(apiUUID string, req * } // Push existing active API keys for this API to the gateway (see BackfillAPIKeysToGateway). - coreservice.BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayEventsService, s.slogger, apiUUID, gatewayID, createdBy) + coreservice.BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayRepo, s.gatewayEventsService, s.slogger, apiUUID, gatewayID, createdBy) } return toAPIDeploymentResponse( @@ -451,7 +451,7 @@ func (s *WebBrokerAPIDeploymentService) restoreWebBrokerAPIDeployment(apiUUID st } // Backfill existing active API keys to the gateway (see BackfillAPIKeysToGateway). - coreservice.BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayEventsService, s.slogger, apiUUID, targetDeployment.GatewayID, "") + coreservice.BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayRepo, s.gatewayEventsService, s.slogger, apiUUID, targetDeployment.GatewayID, "") } return toAPIDeploymentResponse( diff --git a/platform-api/plugins/eventgateway/service/websub_api_deployment.go b/platform-api/plugins/eventgateway/service/websub_api_deployment.go index ba782ab58..e66a86546 100644 --- a/platform-api/plugins/eventgateway/service/websub_api_deployment.go +++ b/platform-api/plugins/eventgateway/service/websub_api_deployment.go @@ -265,7 +265,7 @@ func (s *WebSubAPIDeploymentService) deployWebSubAPI(apiUUID string, req *api.De } // Push existing active API keys for this API to the gateway (see BackfillAPIKeysToGateway). - coreservice.BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayEventsService, s.slogger, apiUUID, gatewayID, createdBy) + coreservice.BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayRepo, s.gatewayEventsService, s.slogger, apiUUID, gatewayID, createdBy) } return toAPIDeploymentResponse( @@ -449,7 +449,7 @@ func (s *WebSubAPIDeploymentService) restoreWebSubAPIDeployment(apiUUID string, } // Backfill existing active API keys to the gateway (see BackfillAPIKeysToGateway). - coreservice.BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayEventsService, s.slogger, apiUUID, targetDeployment.GatewayID, "") + coreservice.BackfillAPIKeysToGateway(s.apiKeyRepo, s.gatewayRepo, s.gatewayEventsService, s.slogger, apiUUID, targetDeployment.GatewayID, "") } return toAPIDeploymentResponse( From e76cc7e0475c205d0d7a823a618714846e591828 Mon Sep 17 00:00:00 2001 From: Dinith Herath Date: Fri, 10 Jul 2026 19:52:44 +0530 Subject: [PATCH 5/5] Add support for creating API keys for LLM proxies/providers without gateway association --- platform-api/internal/service/llm_apikey.go | 5 ----- platform-api/internal/service/llm_proxy_apikey.go | 5 ----- 2 files changed, 10 deletions(-) diff --git a/platform-api/internal/service/llm_apikey.go b/platform-api/internal/service/llm_apikey.go index 364266987..9e43481b4 100644 --- a/platform-api/internal/service/llm_apikey.go +++ b/platform-api/internal/service/llm_apikey.go @@ -249,11 +249,6 @@ func (s *LLMProviderAPIKeyService) CreateLLMProviderAPIKey( return nil, fmt.Errorf("failed to get gateways: %w", err) } - if len(gateways) == 0 { - s.slogger.Warn("No gateways found for organization", "organizationId", orgID) - return nil, apperror.GatewayConnectionUnavailable.Wrap(constants.ErrGatewayUnavailable) - } - apiKeyHashesJSON, err := buildAPIKeyHashesJSON(apiKey, []string{defaultHashingAlgorithm}) if err != nil { s.slogger.Error("Failed to hash API key for LLM provider", "providerId", providerID, "error", err) diff --git a/platform-api/internal/service/llm_proxy_apikey.go b/platform-api/internal/service/llm_proxy_apikey.go index 4181dc041..25c68f2a2 100644 --- a/platform-api/internal/service/llm_proxy_apikey.go +++ b/platform-api/internal/service/llm_proxy_apikey.go @@ -212,11 +212,6 @@ func (s *LLMProxyAPIKeyService) CreateLLMProxyAPIKey( return nil, fmt.Errorf("failed to get gateways: %w", err) } - if len(gateways) == 0 { - s.slogger.Warn("No gateways found for organization", "organizationId", orgID) - return nil, apperror.GatewayConnectionUnavailable.Wrap(constants.ErrGatewayUnavailable) - } - apiKeyHashesJSON, err := buildAPIKeyHashesJSON(apiKey, []string{defaultHashingAlgorithm}) if err != nil { s.slogger.Error("Failed to hash API key for LLM proxy", "proxyId", proxyID, "error", err)