From 4b2072d81e3d47a16126cc3251d51f08eb47a69b Mon Sep 17 00:00:00 2001 From: Austin Engle Date: Thu, 5 Mar 2026 10:15:57 -0700 Subject: [PATCH] feat(integration): add allow_all_models attribute to control model access Add an `allow_all_models` boolean attribute to the `portkey_integration` resource. This controls whether all models are enabled by default for an integration. Defaults to true to match the existing API behavior. When set to false, only models explicitly enabled via `portkey_integration_model_access` resources are available, giving users fine-grained control over which models can be used through an integration. The attribute is managed via the separate `/integrations/{slug}/models` API endpoint since it is not part of the core integration CRUD API. --- CHANGELOG.md | 5 ++ docs/resources/integration.md | 19 +++++ internal/provider/integration_resource.go | 81 ++++++++++++++++++- .../provider/integration_resource_test.go | 66 +++++++++++++++ 4 files changed, 170 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d632a3e..672cb8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- **Integration Model Access Control** - `portkey_integration` now supports `allow_all_models` attribute: + - Defaults to `true` (all models available, matching API behavior) + - Set to `false` to restrict access to only models explicitly enabled via `portkey_integration_model_access` resources + ## [0.2.14] - 2026-02-27 ### Added diff --git a/docs/resources/integration.md b/docs/resources/integration.md index d789e13..cdff4b9 100644 --- a/docs/resources/integration.md +++ b/docs/resources/integration.md @@ -42,6 +42,24 @@ resource "portkey_integration" "openai_dev" { } ``` +### Integration with restricted model access + +```terraform +resource "portkey_integration" "openai_restricted" { + name = "OpenAI Restricted" + ai_provider_id = "openai" + key = var.openai_api_key + allow_all_models = false +} + +# Only enable specific models +resource "portkey_integration_model_access" "gpt4" { + integration_id = portkey_integration.openai_restricted.slug + model_slug = "gpt-4" + enabled = true +} +``` + ## Schema @@ -52,6 +70,7 @@ resource "portkey_integration" "openai_dev" { ### Optional +- `allow_all_models` (Boolean) Whether all models are enabled by default for this integration. When true (the default), all models for the provider are available. Set to false to restrict access to only models explicitly enabled via `portkey_integration_model_access` resources. Defaults to `true`. - `configurations` (String, Sensitive) Provider-specific configurations as JSON. For AWS Bedrock with IAM Role, use: jsonencode({aws_role_arn = "arn:aws:iam::...", aws_region = "us-east-1"}). For Azure OpenAI: jsonencode({azure_auth_mode = "default", azure_resource_name = "...", azure_deployment_config = [{azure_deployment_name = "...", azure_api_version = "...", azure_model_slug = "gpt-4", is_default = true}]}). This is write-only and will not be returned by the API. - `description` (String) Optional description of the integration. - `key` (String, Sensitive) API key for the provider. This is write-only and will not be returned by the API. diff --git a/internal/provider/integration_resource.go b/internal/provider/integration_resource.go index dd56847..6d48dc0 100644 --- a/internal/provider/integration_resource.go +++ b/internal/provider/integration_resource.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/types" @@ -44,6 +45,7 @@ type integrationResourceModel struct { Configurations types.String `tfsdk:"configurations"` Description types.String `tfsdk:"description"` WorkspaceID types.String `tfsdk:"workspace_id"` + AllowAllModels types.Bool `tfsdk:"allow_all_models"` Type types.String `tfsdk:"type"` Status types.String `tfsdk:"status"` CreatedAt types.String `tfsdk:"created_at"` @@ -119,6 +121,12 @@ func (r *integrationResource) Schema(_ context.Context, _ resource.SchemaRequest stringplanmodifier.RequiresReplace(), }, }, + "allow_all_models": schema.BoolAttribute{ + Description: "Whether all models are enabled by default for this integration. When true (the default), all models for the provider are available. Set to false to restrict access to only models explicitly enabled via portkey_integration_model_access resources.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + }, "type": schema.StringAttribute{ Description: "Type of integration: 'organisation' for org-level integrations or 'workspace' for workspace-scoped integrations.", Computed: true, @@ -263,7 +271,9 @@ func (r *integrationResource) Create(ctx context.Context, req resource.CreateReq return } - // Map response body to schema + // Map response body to schema and set partial state BEFORE the models call. + // This ensures that if UpdateIntegrationModels fails, Terraform still tracks + // the created integration and can reconcile on the next plan/apply. plan.ID = types.StringValue(integration.ID) plan.Slug = types.StringValue(integration.Slug) plan.Status = types.StringValue(integration.Status) @@ -289,6 +299,36 @@ func (r *integrationResource) Create(ctx context.Context, req resource.CreateReq } } + // Only call UpdateIntegrationModels when allow_all_models is false, + // since the API already defaults to true. + if !plan.AllowAllModels.ValueBool() { + allowAll := plan.AllowAllModels.ValueBool() + modelsReq := client.BulkUpdateModelsRequest{ + AllowAllModels: &allowAll, + Models: []client.IntegrationModel{}, + } + err = r.client.UpdateIntegrationModels(ctx, createResp.Slug, modelsReq) + if err != nil { + resp.Diagnostics.AddError( + "Error setting allow_all_models", + "Could not update allow_all_models for integration: "+err.Error(), + ) + // State is set below so Terraform tracks the integration even on failure + } + } + + // Read allow_all_models from the models endpoint to get the actual API value + modelsResp, err := r.client.GetIntegrationModels(ctx, integration.Slug) + if err != nil { + // If we can't read models, use the plan value so state is still saved + resp.Diagnostics.AddWarning( + "Error reading integration models after creation", + "Could not read integration models, using plan value: "+err.Error(), + ) + } else { + plan.AllowAllModels = types.BoolValue(modelsResp.AllowAllModels) + } + // Set state to fully populated data diags = resp.State.Set(ctx, plan) resp.Diagnostics.Append(diags...) @@ -352,6 +392,17 @@ func (r *integrationResource) Read(ctx context.Context, req resource.ReadRequest } } + // Read allow_all_models from the models endpoint + modelsResp, err := r.client.GetIntegrationModels(ctx, state.Slug.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Error reading integration models", + "Could not read integration models: "+err.Error(), + ) + return + } + state.AllowAllModels = types.BoolValue(modelsResp.AllowAllModels) + // Set refreshed state diags = resp.State.Set(ctx, &state) resp.Diagnostics.Append(diags...) @@ -464,6 +515,23 @@ func (r *integrationResource) Update(ctx context.Context, req resource.UpdateReq return } + // Update allow_all_models if it changed + if !plan.AllowAllModels.Equal(state.AllowAllModels) { + allowAll := plan.AllowAllModels.ValueBool() + modelsReq := client.BulkUpdateModelsRequest{ + AllowAllModels: &allowAll, + Models: []client.IntegrationModel{}, + } + err = r.client.UpdateIntegrationModels(ctx, state.Slug.ValueString(), modelsReq) + if err != nil { + resp.Diagnostics.AddError( + "Error updating allow_all_models", + "Could not update allow_all_models for integration: "+err.Error(), + ) + return + } + } + // Update resource state with updated items and timestamp plan.ID = types.StringValue(integration.ID) plan.Slug = types.StringValue(integration.Slug) @@ -481,6 +549,17 @@ func (r *integrationResource) Update(ctx context.Context, req resource.UpdateReq // Preserve workspace_id from state (workspace_id is immutable, RequiresReplace) plan.WorkspaceID = state.WorkspaceID + // Read allow_all_models from the models endpoint + modelsResp, err := r.client.GetIntegrationModels(ctx, integration.Slug) + if err != nil { + resp.Diagnostics.AddError( + "Error reading integration models after update", + "Could not read integration models: "+err.Error(), + ) + return + } + plan.AllowAllModels = types.BoolValue(modelsResp.AllowAllModels) + diags = resp.State.Set(ctx, plan) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { diff --git a/internal/provider/integration_resource_test.go b/internal/provider/integration_resource_test.go index bf40196..8087a25 100644 --- a/internal/provider/integration_resource_test.go +++ b/internal/provider/integration_resource_test.go @@ -26,6 +26,7 @@ func TestAccIntegrationResource_basic(t *testing.T) { resource.TestCheckResourceAttr("portkey_integration.test", "ai_provider_id", "openai"), resource.TestCheckResourceAttr("portkey_integration.test", "description", "Initial description"), resource.TestCheckResourceAttr("portkey_integration.test", "status", "active"), + resource.TestCheckResourceAttr("portkey_integration.test", "allow_all_models", "true"), resource.TestCheckResourceAttrSet("portkey_integration.test", "created_at"), ), }, @@ -607,6 +608,71 @@ resource "portkey_integration" "test" { `, name, key) } +func TestAccIntegrationResource_allowAllModelsFalse(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-no-models") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create with allow_all_models = false + { + Config: testAccIntegrationResourceConfigAllowAllModels(rName, false), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("portkey_integration.test", "id"), + resource.TestCheckResourceAttr("portkey_integration.test", "name", rName), + resource.TestCheckResourceAttr("portkey_integration.test", "allow_all_models", "false"), + ), + }, + }, + }) +} + +func TestAccIntegrationResource_allowAllModelsUpdate(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-models-update") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create with allow_all_models = true (default) + { + Config: testAccIntegrationResourceConfigAllowAllModels(rName, true), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("portkey_integration.test", "allow_all_models", "true"), + ), + }, + // Update to allow_all_models = false + { + Config: testAccIntegrationResourceConfigAllowAllModels(rName, false), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("portkey_integration.test", "allow_all_models", "false"), + ), + }, + // Update back to allow_all_models = true + { + Config: testAccIntegrationResourceConfigAllowAllModels(rName, true), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("portkey_integration.test", "allow_all_models", "true"), + ), + }, + }, + }) +} + +func testAccIntegrationResourceConfigAllowAllModels(name string, allowAllModels bool) string { + return fmt.Sprintf(` +provider "portkey" {} + +resource "portkey_integration" "test" { + name = %[1]q + ai_provider_id = "openai" + key = "sk-test-fake-key-12345" + allow_all_models = %[2]t +} +`, name, allowAllModels) +} + func TestAccIntegrationResource_workspaceScoped(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") workspaceID := testAccGetEnvOrSkip(t, "TEST_WORKSPACE_ID")