diff --git a/platform-api/internal/handler/api_key.go b/platform-api/internal/handler/api_key.go index 6547181b7..9d137ec7f 100644 --- a/platform-api/internal/handler/api_key.go +++ b/platform-api/internal/handler/api_key.go @@ -190,7 +190,7 @@ func (h *APIKeyHandler) UpdateAPIKey(w http.ResponseWriter, r *http.Request) { } // Validate that the name in the request body (if provided) matches the URL path parameter - if req.Name != nil && *req.Name != "" && *req.Name != keyName { + if err := utils.ValidateHandleImmutable(keyName, req.Name); err != nil { h.slogger.Warn("API key name mismatch", "userId", userId, "orgId", orgId, "apiHandle", apiHandle, "urlKeyName", keyName, "bodyKeyName", *req.Name) httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", fmt.Sprintf("API key name mismatch: name in request body '%s' must match the key name in URL '%s'", *req.Name, keyName))) diff --git a/platform-api/internal/handler/gateway.go b/platform-api/internal/handler/gateway.go index 611fe55cd..fbbfb3ff5 100644 --- a/platform-api/internal/handler/gateway.go +++ b/platform-api/internal/handler/gateway.go @@ -265,7 +265,7 @@ func (h *GatewayHandler) UpdateGateway(w http.ResponseWriter, r *http.Request) { return } - if req.Id != nil && *req.Id != gatewayId { + if err := utils.ValidateHandleImmutable(gatewayId, req.Id); err != nil { httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Gateway id is immutable and cannot be changed")) return diff --git a/platform-api/internal/handler/llm.go b/platform-api/internal/handler/llm.go index f6c9ef8d9..5029bdee0 100644 --- a/platform-api/internal/handler/llm.go +++ b/platform-api/internal/handler/llm.go @@ -373,6 +373,12 @@ func (h *LLMHandler) UpdateLLMProviderTemplate(w http.ResponseWriter, r *http.Re return } + if err := utils.ValidateHandleImmutable(id, req.Id); err != nil { + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "LLM provider template id is immutable and cannot be changed")) + return + } + updatedBy, ok := resolveActor(w, r, h.identity, h.slogger, "update LLM provider template") if !ok { return @@ -564,6 +570,12 @@ func (h *LLMHandler) UpdateLLMProvider(w http.ResponseWriter, r *http.Request) { return } + if err := utils.ValidateHandleImmutable(id, req.Id); err != nil { + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "LLM provider id is immutable and cannot be changed")) + return + } + updatedBy, ok := resolveActor(w, r, h.identity, h.slogger, "update LLM provider") if !ok { return @@ -815,6 +827,12 @@ func (h *LLMHandler) UpdateLLMProxy(w http.ResponseWriter, r *http.Request) { return } + if err := utils.ValidateHandleImmutable(id, req.Id); err != nil { + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "LLM proxy id is immutable and cannot be changed")) + return + } + updatedBy, ok := resolveActor(w, r, h.identity, h.slogger, "update LLM proxy") if !ok { return diff --git a/platform-api/internal/handler/mcp.go b/platform-api/internal/handler/mcp.go index 12816829f..fa26820e1 100644 --- a/platform-api/internal/handler/mcp.go +++ b/platform-api/internal/handler/mcp.go @@ -175,6 +175,12 @@ func (h *MCPProxyHandler) UpdateMCPProxy(w http.ResponseWriter, r *http.Request) return } + if err := utils.ValidateHandleImmutable(id, req.Id); err != nil { + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "MCP proxy id is immutable and cannot be changed")) + return + } + updatedBy, ok := resolveActor(w, r, h.identity, h.slogger, "update MCP proxy") if !ok { return diff --git a/platform-api/internal/handler/project.go b/platform-api/internal/handler/project.go index 751f1d1f5..61426a79b 100644 --- a/platform-api/internal/handler/project.go +++ b/platform-api/internal/handler/project.go @@ -184,6 +184,16 @@ func (h *ProjectHandler) UpdateProject(w http.ResponseWriter, r *http.Request) { return } + var reqId string + if req.Id != nil { + reqId = *req.Id + } + if err := utils.ValidateHandleImmutableRequired(projectId, reqId); err != nil { + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Project id is immutable and cannot be changed")) + return + } + actor, ok := resolveActor(w, r, h.identity, h.slogger, "update project") if !ok { return diff --git a/platform-api/internal/handler/subscription_plan_handler.go b/platform-api/internal/handler/subscription_plan_handler.go index 5f3f17cfe..ba898ebc6 100644 --- a/platform-api/internal/handler/subscription_plan_handler.go +++ b/platform-api/internal/handler/subscription_plan_handler.go @@ -326,7 +326,7 @@ func (h *SubscriptionPlanHandler) UpdateSubscriptionPlan(w http.ResponseWriter, return } - if req.Id != nil && *req.Id != "" && *req.Id != planId { + if err := utils.ValidateHandleImmutable(planId, req.Id); err != nil { httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "The plan id is immutable and cannot be changed")) return diff --git a/platform-api/internal/service/api.go b/platform-api/internal/service/api.go index 30424bb01..f041c7b27 100644 --- a/platform-api/internal/service/api.go +++ b/platform-api/internal/service/api.go @@ -419,8 +419,10 @@ func (s *APIService) DeleteAPI(apiUUID, orgUUID, deletedBy string) error { // UpdateAPIByHandle updates an existing API identified by handle func (s *APIService) UpdateAPIByHandle(handle string, req *api.RESTAPI, orgId, updatedBy string) (*api.RESTAPI, error) { // The id (handle) is immutable: a body id must match the API being updated. - if req != nil && req.Id != nil && *req.Id != "" && *req.Id != handle { - return nil, constants.ErrHandleImmutable + if req != nil { + if err := utils.ValidateHandleImmutable(handle, req.Id); err != nil { + return nil, err + } } apiUUID, err := s.getAPIUUIDByHandle(handle, orgId) if err != nil { diff --git a/platform-api/internal/service/application.go b/platform-api/internal/service/application.go index 87cd70a3d..7d5c44a34 100644 --- a/platform-api/internal/service/application.go +++ b/platform-api/internal/service/application.go @@ -269,8 +269,8 @@ func (s *ApplicationService) UpdateApplication(appIDOrHandle string, req *api.Ap } // The id (handle) is immutable: body id must be present and match the application being updated. - if req.Id == "" || req.Id != app.Handle { - return nil, constants.ErrHandleImmutable + if err := utils.ValidateHandleImmutableRequired(app.Handle, req.Id); err != nil { + return nil, err } name := strings.TrimSpace(req.DisplayName) diff --git a/platform-api/internal/service/llm.go b/platform-api/internal/service/llm.go index ea4a55c2c..ba584af7e 100644 --- a/platform-api/internal/service/llm.go +++ b/platform-api/internal/service/llm.go @@ -325,9 +325,6 @@ func (s *LLMProviderTemplateService) Update(orgUUID, handle, updatedBy string, r if handle == "" || req == nil { return nil, constants.ErrInvalidInput } - if req.Id != nil && *req.Id != "" && *req.Id != handle { - return nil, constants.ErrHandleImmutable - } if req.DisplayName == "" { return nil, constants.ErrInvalidInput } @@ -1030,9 +1027,6 @@ func (s *LLMProviderService) Update(orgUUID, handle, updatedBy string, req *api. if handle == "" || req == nil { return nil, constants.ErrInvalidInput } - if req.Id != nil && *req.Id != "" && *req.Id != handle { - return nil, constants.ErrHandleImmutable - } // Fetch existing provider to preserve sensitive fields on update existing, err := s.repo.GetByID(handle, orgUUID) if err != nil { @@ -1482,9 +1476,6 @@ func (s *LLMProxyService) Update(orgUUID, handle, updatedBy string, req *api.LLM if handle == "" || req == nil { return nil, constants.ErrInvalidInput } - if req.Id != nil && *req.Id != "" && *req.Id != handle { - return nil, constants.ErrHandleImmutable - } if req.DisplayName == "" || req.Version == "" || req.Provider.Id == "" { return nil, constants.ErrInvalidInput } diff --git a/platform-api/internal/service/llm_provider_template_test.go b/platform-api/internal/service/llm_provider_template_test.go index 9d4d0948d..b26ea6366 100644 --- a/platform-api/internal/service/llm_provider_template_test.go +++ b/platform-api/internal/service/llm_provider_template_test.go @@ -378,19 +378,6 @@ func TestLLMProviderTemplateServiceUpdate_PropagatesNameToFamily(t *testing.T) { } } -func TestLLMProviderTemplateServiceUpdate_RejectsMismatchedID(t *testing.T) { - repo := &mockLLMProviderTemplateCRUDRepo{managedByForHandleResult: "customer"} - svc := NewLLMProviderTemplateService(repo, &noopAuditRepo{}, newTestIdentityService()) - - req := validTemplateRequest("Name") - otherHandle := "some-other-handle" - req.Id = &otherHandle - _, err := svc.Update("org-1", "mistralai", "alice", req) - if !errors.Is(err, constants.ErrHandleImmutable) { - t.Fatalf("expected ErrHandleImmutable, got: %v", err) - } -} - // ---- CreateVersion ---- func TestLLMProviderTemplateServiceCreateVersion_Success(t *testing.T) { diff --git a/platform-api/internal/service/mcp.go b/platform-api/internal/service/mcp.go index 0be71eb94..5ece56e73 100644 --- a/platform-api/internal/service/mcp.go +++ b/platform-api/internal/service/mcp.go @@ -334,9 +334,6 @@ func (s *MCPProxyService) Update(orgUUID, handle, updatedBy string, req *api.MCP if handle == "" || req == nil { return nil, constants.ErrInvalidInput } - if req.Id != nil && *req.Id != "" && *req.Id != handle { - return nil, constants.ErrHandleImmutable - } if req.DisplayName == "" || req.Version == "" { return nil, constants.ErrInvalidInput } diff --git a/platform-api/internal/service/project.go b/platform-api/internal/service/project.go index 397bfbc42..ece7557f5 100644 --- a/platform-api/internal/service/project.go +++ b/platform-api/internal/service/project.go @@ -207,11 +207,6 @@ func (s *ProjectService) UpdateProject(handle string, req *api.Project, orgId, a return nil, constants.ErrProjectNotFound } - // Validate that the handle in the body matches the path param (immutability check) - if req.Id == nil || *req.Id != handle { - return nil, constants.ErrHandleImmutable - } - if req.DisplayName != project.Name { existingProjects, err := s.projectRepo.GetProjectsByOrganizationID(project.OrganizationID) if err != nil { diff --git a/platform-api/internal/service/subscription_plan_service.go b/platform-api/internal/service/subscription_plan_service.go index b688613a9..bd2b7828c 100644 --- a/platform-api/internal/service/subscription_plan_service.go +++ b/platform-api/internal/service/subscription_plan_service.go @@ -165,10 +165,6 @@ func (s *SubscriptionPlanService) UpdatePlan(handle, orgUUID, actor string, upda return nil, constants.ErrSubscriptionPlanNotFound } - // The handle (id) is immutable; reject any attempt to change it to a different value. - if update.Handle != nil && *update.Handle != "" && *update.Handle != existing.Handle { - return nil, constants.ErrHandleImmutable - } if update.Name != nil { if *update.Name == "" { return nil, fmt.Errorf("name is required") diff --git a/platform-api/internal/utils/handle.go b/platform-api/internal/utils/handle.go index 40efc1a76..f1ad01a31 100644 --- a/platform-api/internal/utils/handle.go +++ b/platform-api/internal/utils/handle.go @@ -42,6 +42,25 @@ var ( multipleHyphensRegex = regexp.MustCompile(`-+`) ) +// ValidateHandleImmutable checks a PUT request body's optional handle/id field against +// the resource's path handle. A nil or empty body value is treated as "not provided" +// and allowed; only a non-empty value that differs from the path handle is rejected. +func ValidateHandleImmutable(pathHandle string, bodyHandle *string) error { + if bodyHandle == nil || *bodyHandle == "" { + return nil + } + return ValidateHandleImmutableRequired(pathHandle, *bodyHandle) +} + +// ValidateHandleImmutableRequired is like ValidateHandleImmutable but requires the +// request body to explicitly include a handle/id value matching the path handle. +func ValidateHandleImmutableRequired(pathHandle, bodyHandle string) error { + if bodyHandle == "" || bodyHandle != pathHandle { + return constants.ErrHandleImmutable + } + return nil +} + // ValidateHandle validates a user-provided handle. // Handle must be: // - Lowercase only diff --git a/platform-api/plugins/eventgateway/handler/webbroker_api.go b/platform-api/plugins/eventgateway/handler/webbroker_api.go index 41fd86750..43087858a 100644 --- a/platform-api/plugins/eventgateway/handler/webbroker_api.go +++ b/platform-api/plugins/eventgateway/handler/webbroker_api.go @@ -170,6 +170,12 @@ func (h *WebBrokerAPIHandler) UpdateWebBrokerAPI(w http.ResponseWriter, r *http. return } + if err := utils.ValidateHandleImmutable(id, req.Id); err != nil { + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "WebBroker API id is immutable and cannot be changed")) + return + } + updatedBy, ok := resolveActor(w, r, h.identity, h.slogger, "update WebBroker API") if !ok { return diff --git a/platform-api/plugins/eventgateway/handler/webbroker_apikey.go b/platform-api/plugins/eventgateway/handler/webbroker_apikey.go index 1d7c79a71..7412f9d61 100644 --- a/platform-api/plugins/eventgateway/handler/webbroker_apikey.go +++ b/platform-api/plugins/eventgateway/handler/webbroker_apikey.go @@ -175,7 +175,7 @@ func (h *WebBrokerAPIKeyHandler) UpdateAPIKey(w http.ResponseWriter, r *http.Req } // Validate that the name in the request body (if provided) matches the URL path parameter - if req.Name != nil && *req.Name != "" && *req.Name != keyName { + if err := utils.ValidateHandleImmutable(keyName, req.Name); err != nil { h.slogger.Warn("API key name mismatch", "orgId", orgID, "apiHandle", apiHandle, "urlKeyName", keyName, "bodyKeyName", *req.Name) httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", fmt.Sprintf("API key name mismatch: name in request body '%s' must match the key name in URL '%s'", *req.Name, keyName))) diff --git a/platform-api/plugins/eventgateway/handler/websub_api.go b/platform-api/plugins/eventgateway/handler/websub_api.go index 7498d3cf0..f1ec3305b 100644 --- a/platform-api/plugins/eventgateway/handler/websub_api.go +++ b/platform-api/plugins/eventgateway/handler/websub_api.go @@ -161,6 +161,12 @@ func (h *WebSubAPIHandler) UpdateWebSubAPI(w http.ResponseWriter, r *http.Reques return } + if err := utils.ValidateHandleImmutable(id, req.Id); err != nil { + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "WebSub API id is immutable and cannot be changed")) + return + } + updatedBy, ok := resolveActor(w, r, h.identity, h.slogger, "update WebSub API") if !ok { return diff --git a/platform-api/plugins/eventgateway/handler/websub_apikey.go b/platform-api/plugins/eventgateway/handler/websub_apikey.go index 94a88edcb..b4df96b9c 100644 --- a/platform-api/plugins/eventgateway/handler/websub_apikey.go +++ b/platform-api/plugins/eventgateway/handler/websub_apikey.go @@ -175,7 +175,7 @@ func (h *WebSubAPIKeyHandler) UpdateAPIKey(w http.ResponseWriter, r *http.Reques } // Validate that the name in the request body (if provided) matches the URL path parameter - if req.Name != nil && *req.Name != "" && *req.Name != keyName { + if err := utils.ValidateHandleImmutable(keyName, req.Name); err != nil { h.slogger.Warn("API key name mismatch", "orgId", orgID, "apiHandle", apiHandle, "urlKeyName", keyName, "bodyKeyName", *req.Name) httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", fmt.Sprintf("API key name mismatch: name in request body '%s' must match the key name in URL '%s'", *req.Name, keyName))) diff --git a/platform-api/plugins/eventgateway/service/webbroker_api.go b/platform-api/plugins/eventgateway/service/webbroker_api.go index 354473246..788280843 100644 --- a/platform-api/plugins/eventgateway/service/webbroker_api.go +++ b/platform-api/plugins/eventgateway/service/webbroker_api.go @@ -263,9 +263,6 @@ func (s *WebBrokerAPIService) Update(orgUUID, handle, updatedBy string, req *api if handle == "" || req == nil { return nil, constants.ErrInvalidInput } - if req.Id != nil && *req.Id != "" && *req.Id != handle { - return nil, constants.ErrHandleImmutable - } if req.DisplayName == "" || req.Version == "" { return nil, constants.ErrInvalidInput } diff --git a/platform-api/plugins/eventgateway/service/websub_api.go b/platform-api/plugins/eventgateway/service/websub_api.go index 247f18eb0..89d0cf7cc 100644 --- a/platform-api/plugins/eventgateway/service/websub_api.go +++ b/platform-api/plugins/eventgateway/service/websub_api.go @@ -262,9 +262,6 @@ func (s *WebSubAPIService) Update(orgUUID, handle, updatedBy string, req *api.We if handle == "" || req == nil { return nil, constants.ErrInvalidInput } - if req.Id != nil && *req.Id != "" && *req.Id != handle { - return nil, constants.ErrHandleImmutable - } if req.DisplayName == "" || req.Version == "" { return nil, constants.ErrInvalidInput }