From db7f4438b4ad189390c996a1918549da461200b7 Mon Sep 17 00:00:00 2001 From: Yevhenii Shcherbina Date: Wed, 1 Jul 2026 16:44:15 -0400 Subject: [PATCH] feat: generate STS external ID for Bedrock role assumption (#26869) Implements: https://linear.app/codercom/issue/AIGOV-495/add-externalid-to-prevent-confused-deputy-problem When a Bedrock provider assumes an IAM role via STS, the gateway now generates a unique external ID for it and sends that value on every `AssumeRole` call. The external ID guards against the [confused deputy problem](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html) on cross-account role assumption. Per [AWS's recommendation](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html), the gateway generates and owns the value rather than accepting one from the operator; that ownership is what makes it effective, since a party who knows another's external ID can't induce the gateway to send it. The external ID is server-owned and read-only over the API. It is generated once, when a provider first has a `role_arn`, and is stable thereafter. Clients cannot set it: create rejects any supplied `external_id`, and update rejects a value that differs from the stored one. An update may echo the stored value back unchanged, so the normal read-modify-write flow (GET the provider, change a field, PATCH the full settings object) keeps working. The value is not a secret and is returned on GET so operators can copy it into the target role's trust policy as an `sts:ExternalId` condition. It is persisted in the existing JSON settings blob, so there is no migration or audit-table change. --- aibridge/config/config.go | 4 + aibridge/provider/bedrock.go | 3 + aibridge/provider/bedrock_internal_test.go | 55 +++++ cli/aibridged.go | 2 + coderd/ai_providers.go | 55 +++++ coderd/ai_providers_internal_test.go | 89 ++++++++ coderd/ai_providers_test.go | 223 +++++++++++++++++++++ coderd/aibridged/proto/aibridged.pb.go | 142 +++++++------ coderd/aibridged/proto/aibridged.proto | 1 + coderd/aibridgedserver/aibridgedserver.go | 1 + codersdk/aiproviders.go | 6 + codersdk/aiproviders_bedrock.go | 5 + site/src/api/typesGenerated.ts | 7 + 13 files changed, 527 insertions(+), 66 deletions(-) create mode 100644 coderd/ai_providers_internal_test.go diff --git a/aibridge/config/config.go b/aibridge/config/config.go index 3dc76841aa..b01282b0cc 100644 --- a/aibridge/config/config.go +++ b/aibridge/config/config.go @@ -38,6 +38,10 @@ type AWSBedrock struct { // IRSA / EKS Pod Identity / EC2 Instance Profile) signs the AssumeRole // call, and the resulting temporary credentials sign Bedrock requests. RoleARN string + // ExternalID is sent as the STS external ID on the AssumeRole call. + // It is meaningful only alongside RoleARN and must match the + // sts:ExternalId condition on the target role's trust policy. + ExternalID string } // OpenAI carries configuration for an OpenAI provider. diff --git a/aibridge/provider/bedrock.go b/aibridge/provider/bedrock.go index 93bbf7038a..013430f1d7 100644 --- a/aibridge/provider/bedrock.go +++ b/aibridge/provider/bedrock.go @@ -79,6 +79,9 @@ func buildBedrockCredentials(ctx context.Context, cfg config.AWSBedrock) (aws.Cr if cfg.RoleARN != "" { credsProvider = stscreds.NewAssumeRoleProvider(sts.NewFromConfig(base), cfg.RoleARN, func(o *stscreds.AssumeRoleOptions) { o.RoleSessionName = bedrockSessionName + if cfg.ExternalID != "" { + o.ExternalID = aws.String(cfg.ExternalID) + } }) credsProvider = aws.NewCredentialsCache(credsProvider) } diff --git a/aibridge/provider/bedrock_internal_test.go b/aibridge/provider/bedrock_internal_test.go index 0ecae6c780..1cb7764c89 100644 --- a/aibridge/provider/bedrock_internal_test.go +++ b/aibridge/provider/bedrock_internal_test.go @@ -207,6 +207,61 @@ func TestBuildBedrockCredentialsAssumeRole(t *testing.T) { require.Equal(t, bedrockSessionName, gotSessionName) } +// TestBuildBedrockCredentialsAssumeRoleExternalID verifies that a configured +// external ID is sent on the STS AssumeRole call, and that omitting it sends +// no ExternalId parameter. +// NOTE: no t.Parallel() because it uses t.Setenv. +func TestBuildBedrockCredentialsAssumeRoleExternalID(t *testing.T) { + tests := []struct { + name string + externalID string + wantExternalID string + }{ + {name: "with external id", externalID: "trust-policy-id-123", wantExternalID: "trust-policy-id-123"}, + {name: "without external id", externalID: "", wantExternalID: ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var gotExternalID string + // Mock the AWS STS AssumeRole API. + // https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html + sts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.NoError(t, r.ParseForm()) + gotExternalID = r.Form.Get("ExternalId") + + w.Header().Set("Content-Type", "text/xml") + _, _ = w.Write([]byte(` + + + ASIAASSUMED + assumed-secret + assumed-token + 2999-01-01T00:00:00Z + + +`)) + })) + defer sts.Close() + + t.Setenv("AWS_ENDPOINT_URL_STS", sts.URL) + t.Setenv("AWS_ACCESS_KEY_ID", "base-key") + t.Setenv("AWS_SECRET_ACCESS_KEY", "base-secret") + + creds, _, err := buildBedrockCredentials(context.Background(), config.AWSBedrock{ + Region: "us-east-1", + RoleARN: "arn:aws:iam::123456789012:role/target", + ExternalID: tt.externalID, + }) + require.NoError(t, err) + + _, err = creds.Retrieve(context.Background()) + require.NoError(t, err) + require.Equal(t, tt.wantExternalID, gotExternalID) + }) + } +} + // TestBuildBedrockCredentialsAssumeRoleError verifies that when STS rejects the // AssumeRole call (e.g. a trust-policy or IAM denial), the failure surfaces to // the caller on Retrieve with enough detail to diagnose it, rather than being diff --git a/cli/aibridged.go b/cli/aibridged.go index 87e583b573..addeb3f959 100644 --- a/cli/aibridged.go +++ b/cli/aibridged.go @@ -211,6 +211,7 @@ func protoToProviderSpec(pp *proto.AIProvider) aiProviderSpec { b.GetSmallFastModel(), ) bedrock.RoleARN = b.GetRoleArn() + bedrock.ExternalID = b.GetExternalId() spec.Bedrock = ptr.Ref(bedrock) } return spec @@ -352,6 +353,7 @@ func bedrockConfig(baseURL string, bedrock *codersdk.AIProviderBedrockSettings) Model: bedrockSettings.Model, SmallFastModel: bedrockSettings.SmallFastModel, RoleARN: bedrockSettings.RoleARN, + ExternalID: bedrockSettings.ExternalID, } } diff --git a/coderd/ai_providers.go b/coderd/ai_providers.go index 49e5bb8750..a00e026800 100644 --- a/coderd/ai_providers.go +++ b/coderd/ai_providers.go @@ -2,6 +2,7 @@ package coderd import ( "context" + "crypto/rand" "database/sql" "encoding/json" "errors" @@ -178,6 +179,9 @@ func (api *API) aiProvidersCreate(rw http.ResponseWriter, r *http.Request) { return } + // Generate the server-owned external ID when the provider assumes a role. + ensureBedrockExternalID(&req.Settings) + settings, err := encodeAIProviderSettings(req.Settings) if err != nil { api.Logger.Error(ctx, "encode AI provider settings", slog.Error(err)) @@ -318,6 +322,9 @@ func (api *API) aiProvidersUpdate(rw http.ResponseWriter, r *http.Request) { return xerrors.Errorf("decode existing settings: %w", err) } if req.Settings != nil { + if err := validateBedrockExternalIDUnchanged(existing, *req.Settings); err != nil { + return err + } existing = mergeAIProviderSettings(existing, *req.Settings) } // Bedrock settings are only meaningful for anthropic- or @@ -329,6 +336,9 @@ func (api *API) aiProvidersUpdate(rw http.ResponseWriter, r *http.Request) { old.Type != database.AIProviderTypeBedrock { return errAIProviderBedrockTypeMismatch } + // Generate the server-owned external ID when the provider assumes a role + // and lacks one. + ensureBedrockExternalID(&existing) settings, err := encodeAIProviderSettings(existing) if err != nil { return xerrors.Errorf("encode settings: %w", err) @@ -400,6 +410,12 @@ func (api *API) aiProvidersUpdate(rw http.ResponseWriter, r *http.Request) { }) return } + if errors.Is(err, errAIProviderExternalIDReadOnly) { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "The Bedrock external ID is server-generated and cannot be changed.", + }) + return + } if errors.Is(err, errAIProviderKeyUnknown) { // Use the sentinel directly so the response message does not // leak the "execute transaction:" wrapper xerrors added on the @@ -506,6 +522,12 @@ var errCopilotRejectsAPIKeys = xerrors.New("copilot providers do not accept api_ // the outer handler translates it into a 400. var errAIProviderBedrockTypeMismatch = xerrors.New("bedrock settings are only valid for type=anthropic or type=bedrock") +// errAIProviderExternalIDReadOnly is the sentinel returned from inside +// the update transaction when a patch tries to change the server-owned +// Bedrock external ID; the outer handler translates it into a 400. A +// patch may echo the stored value but not set a different one. +var errAIProviderExternalIDReadOnly = xerrors.New("external_id is server-generated and cannot be changed") + // errAIProviderInvalidName is returned from lookupAIProvider when the // idOrName parameter is neither a UUID nor a syntactically-valid name. // The handler translates this into a 400 so an integrator gets a hint @@ -772,6 +794,39 @@ func mergeAIProviderSettings(existing, patch codersdk.AIProviderSettings) coders if merged.AccessKeySecret == nil { merged.AccessKeySecret = existing.Bedrock.AccessKeySecret } + // The external ID is server-owned and stable: carry the stored value + // forward so a patch can't change it. A patch that sets a different + // value is rejected upstream. + merged.ExternalID = existing.Bedrock.ExternalID } return codersdk.AIProviderSettings{Bedrock: &merged} } + +// validateBedrockExternalIDUnchanged rejects a patch that sets a Bedrock +// external ID different from the stored one. A patch may echo the stored +// value (read-modify-write resends it) but not change it; the value is +// server-owned. +func validateBedrockExternalIDUnchanged(existing, patch codersdk.AIProviderSettings) error { + stored := "" + if existing.Bedrock != nil { + stored = existing.Bedrock.ExternalID + } + + provided := "" + if patch.Bedrock != nil { + provided = patch.Bedrock.ExternalID + } + + if provided != "" && provided != stored { + return errAIProviderExternalIDReadOnly + } + return nil +} + +// ensureBedrockExternalID assigns a server-owned STS external ID when the +// Bedrock provider assumes a role and none is set yet. +func ensureBedrockExternalID(s *codersdk.AIProviderSettings) { + if s.Bedrock != nil && s.Bedrock.RoleARN != "" && s.Bedrock.ExternalID == "" { + s.Bedrock.ExternalID = rand.Text() + } +} diff --git a/coderd/ai_providers_internal_test.go b/coderd/ai_providers_internal_test.go new file mode 100644 index 0000000000..102dd9ac45 --- /dev/null +++ b/coderd/ai_providers_internal_test.go @@ -0,0 +1,89 @@ +package coderd + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/codersdk" +) + +// TestEnsureBedrockExternalID covers the server-owned external ID generation: +// it generates only when a role is configured and none is set, and never +// overwrites an existing value. +func TestEnsureBedrockExternalID(t *testing.T) { + t.Parallel() + + t.Run("NilBedrockIsNoOp", func(t *testing.T) { + t.Parallel() + s := codersdk.AIProviderSettings{} + ensureBedrockExternalID(&s) + require.Nil(t, s.Bedrock) + }) + + t.Run("NoRoleLeavesEmpty", func(t *testing.T) { + t.Parallel() + s := codersdk.AIProviderSettings{Bedrock: &codersdk.AIProviderBedrockSettings{Region: "us-east-1"}} + ensureBedrockExternalID(&s) + require.Empty(t, s.Bedrock.ExternalID) + }) + + t.Run("GeneratesWhenRoleSet", func(t *testing.T) { + t.Parallel() + s := codersdk.AIProviderSettings{Bedrock: &codersdk.AIProviderBedrockSettings{ + RoleARN: "arn:aws:iam::123456789012:role/BedrockRole", + }} + ensureBedrockExternalID(&s) + // The bounds are a sanity floor and ceiling, not a correctness + // requirement. crypto/rand.Text() currently returns 26 chars, but + // its docs allow future Go versions to return longer text. If a Go + // upgrade trips these bounds, widen them or use different function. + require.GreaterOrEqual(t, len(s.Bedrock.ExternalID), 26) + require.LessOrEqual(t, len(s.Bedrock.ExternalID), 52) + }) + + t.Run("DoesNotOverwriteExisting", func(t *testing.T) { + t.Parallel() + s := codersdk.AIProviderSettings{Bedrock: &codersdk.AIProviderBedrockSettings{ + RoleARN: "arn:aws:iam::123456789012:role/BedrockRole", + ExternalID: "existing-value", + }} + ensureBedrockExternalID(&s) + require.Equal(t, "existing-value", s.Bedrock.ExternalID) + }) + + t.Run("GeneratesUniqueValues", func(t *testing.T) { + t.Parallel() + seen := make(map[string]struct{}) + for range 10 { + s := codersdk.AIProviderSettings{Bedrock: &codersdk.AIProviderBedrockSettings{ + RoleARN: "arn:aws:iam::123456789012:role/BedrockRole", + }} + ensureBedrockExternalID(&s) + _, dup := seen[s.Bedrock.ExternalID] + require.False(t, dup, "external IDs must be unique per provider") + seen[s.Bedrock.ExternalID] = struct{}{} + } + }) +} + +// TestMergeAIProviderSettingsExternalID verifies the external ID is treated as +// server-owned during a PATCH merge: a stored value is carried forward and +// overrides the patch so it can't be changed. +func TestMergeAIProviderSettingsExternalID(t *testing.T) { + t.Parallel() + + roleARN := "arn:aws:iam::123456789012:role/BedrockRole" + existing := codersdk.AIProviderSettings{Bedrock: &codersdk.AIProviderBedrockSettings{ + RoleARN: roleARN, + ExternalID: "stored-value", + }} + patch := codersdk.AIProviderSettings{Bedrock: &codersdk.AIProviderBedrockSettings{ + RoleARN: roleARN, + ExternalID: "client-supplied-value", + }} + merged := mergeAIProviderSettings(existing, patch) + require.NotNil(t, merged.Bedrock) + require.Equal(t, roleARN, merged.Bedrock.RoleARN) + require.Equal(t, "stored-value", merged.Bedrock.ExternalID) +} diff --git a/coderd/ai_providers_test.go b/coderd/ai_providers_test.go index 4fcc1dd63b..32e6b8947d 100644 --- a/coderd/ai_providers_test.go +++ b/coderd/ai_providers_test.go @@ -1554,3 +1554,226 @@ func TestAIProviderSettingsMerge(t *testing.T) { require.Equal(t, "", *persisted.Bedrock.AccessKeySecret) }) } + +// TestAIProvidersBedrockExternalID covers the server-owned STS external ID: +// it is generated when (and only when) the provider assumes a role, is +// rejected when a client tries to set or change it, and is stable across +// PATCHes that echo the stored value. +func TestAIProvidersBedrockExternalID(t *testing.T) { + t.Parallel() + + const ( + roleARN = "arn:aws:iam::123456789012:role/BedrockRole" + externalIDReadOnlyMsg = "The Bedrock external ID is server-generated and cannot be changed." + ) + + createBedrock := func(t *testing.T, client *codersdk.Client, name string, b codersdk.AIProviderBedrockSettings) (codersdk.AIProvider, error) { + t.Helper() + ctx := testutil.Context(t, testutil.WaitLong) + //nolint:gocritic // Owner role is the audience for this endpoint. + return client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{ + Type: codersdk.AIProviderTypeBedrock, + Name: name, + Enabled: true, + BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com", + Settings: codersdk.AIProviderSettings{Bedrock: &b}, + }) + } + + t.Run("GeneratedWhenRoleSet", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + ctx := testutil.Context(t, testutil.WaitLong) + + created, err := createBedrock(t, client, "bedrock-role", codersdk.AIProviderBedrockSettings{ + Region: "us-east-1", + RoleARN: roleARN, + }) + require.NoError(t, err) + require.NotNil(t, created.Settings.Bedrock) + require.NotEmpty(t, created.Settings.Bedrock.ExternalID, "external ID must be generated when a role is set") + + // GET returns the same external ID. + got, err := client.AIProvider(ctx, created.ID.String()) + require.NoError(t, err) + require.Equal(t, created.Settings.Bedrock.ExternalID, got.Settings.Bedrock.ExternalID) + }) + + t.Run("AbsentWithoutRole", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + + created, err := createBedrock(t, client, "bedrock-no-role", codersdk.AIProviderBedrockSettings{Region: "us-east-1"}) + require.NoError(t, err) + require.NotNil(t, created.Settings.Bedrock) + require.Empty(t, created.Settings.Bedrock.ExternalID, "no external ID without a role to assume") + }) + + t.Run("RejectsClientValueOnCreate", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + + _, err := createBedrock(t, client, "bedrock-client-id", codersdk.AIProviderBedrockSettings{ + Region: "us-east-1", + RoleARN: roleARN, + ExternalID: "client-supplied-value", + }) + sdkErr := requireSDKError(t, err, http.StatusBadRequest) + require.Contains(t, sdkErr.Validations, codersdk.ValidationError{ + Field: "settings.external_id", + Detail: "external_id is server-generated and cannot be set", + }) + }) + + t.Run("StableWhenPatchOmitsValue", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + ctx := testutil.Context(t, testutil.WaitLong) + + created, err := createBedrock(t, client, "bedrock-stable", codersdk.AIProviderBedrockSettings{ + Region: "us-east-1", + RoleARN: roleARN, + }) + require.NoError(t, err) + original := created.Settings.Bedrock.ExternalID + require.NotEmpty(t, original) + + updated, err := client.UpdateAIProvider(ctx, created.Name, codersdk.UpdateAIProviderRequest{ + Settings: &codersdk.AIProviderSettings{ + Bedrock: &codersdk.AIProviderBedrockSettings{Region: "us-west-2", RoleARN: roleARN}, + }, + }) + require.NoError(t, err) + require.Equal(t, "us-west-2", updated.Settings.Bedrock.Region) + require.Equal(t, original, updated.Settings.Bedrock.ExternalID, "external ID must be stable across PATCH") + }) + + t.Run("StableAcrossRoleRemovalAndReassignment", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + ctx := testutil.Context(t, testutil.WaitLong) + + const roleB = "arn:aws:iam::123456789012:role/BedrockRoleB" + + created, err := createBedrock(t, client, "bedrock-toggle", codersdk.AIProviderBedrockSettings{ + Region: "us-east-1", + RoleARN: roleARN, + }) + require.NoError(t, err) + original := created.Settings.Bedrock.ExternalID + require.NotEmpty(t, original) + + // Removing the role retains the external ID. + cleared, err := client.UpdateAIProvider(ctx, created.Name, codersdk.UpdateAIProviderRequest{ + Settings: &codersdk.AIProviderSettings{ + Bedrock: &codersdk.AIProviderBedrockSettings{Region: "us-east-1"}, + }, + }) + require.NoError(t, err) + require.Empty(t, cleared.Settings.Bedrock.RoleARN) + require.Equal(t, original, cleared.Settings.Bedrock.ExternalID) + + // Adding a different role reuses the retained ID rather than + // regenerating it, so a trust policy referencing it keeps working. + readded, err := client.UpdateAIProvider(ctx, created.Name, codersdk.UpdateAIProviderRequest{ + Settings: &codersdk.AIProviderSettings{ + Bedrock: &codersdk.AIProviderBedrockSettings{Region: "us-east-1", RoleARN: roleB}, + }, + }) + require.NoError(t, err) + require.Equal(t, roleB, readded.Settings.Bedrock.RoleARN) + require.Equal(t, original, readded.Settings.Bedrock.ExternalID, "external ID must survive role removal and re-add") + }) + + t.Run("AllowsEchoedValueOnPatch", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + ctx := testutil.Context(t, testutil.WaitLong) + + created, err := createBedrock(t, client, "bedrock-echo", codersdk.AIProviderBedrockSettings{ + Region: "us-east-1", + RoleARN: roleARN, + }) + require.NoError(t, err) + original := created.Settings.Bedrock.ExternalID + require.NotEmpty(t, original) + + // Read-modify-write resends the whole settings, including the stored + // external ID. Echoing the same value is allowed. + updated, err := client.UpdateAIProvider(ctx, created.Name, codersdk.UpdateAIProviderRequest{ + Settings: &codersdk.AIProviderSettings{ + Bedrock: &codersdk.AIProviderBedrockSettings{Region: "us-west-2", RoleARN: roleARN, ExternalID: original}, + }, + }) + require.NoError(t, err) + require.Equal(t, "us-west-2", updated.Settings.Bedrock.Region) + require.Equal(t, original, updated.Settings.Bedrock.ExternalID) + }) + + t.Run("RejectsChangedValueOnPatch", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + ctx := testutil.Context(t, testutil.WaitLong) + + created, err := createBedrock(t, client, "bedrock-change", codersdk.AIProviderBedrockSettings{ + Region: "us-east-1", + RoleARN: roleARN, + }) + require.NoError(t, err) + require.NotEmpty(t, created.Settings.Bedrock.ExternalID) + + _, err = client.UpdateAIProvider(ctx, created.Name, codersdk.UpdateAIProviderRequest{ + Settings: &codersdk.AIProviderSettings{ + Bedrock: &codersdk.AIProviderBedrockSettings{Region: "us-east-1", RoleARN: roleARN, ExternalID: "client-tries-to-change-it"}, + }, + }) + sdkErr := requireSDKError(t, err, http.StatusBadRequest) + require.Equal(t, externalIDReadOnlyMsg, sdkErr.Message) + }) + + t.Run("GeneratedWhenRoleAddedByPatch", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + ctx := testutil.Context(t, testutil.WaitLong) + + created, err := createBedrock(t, client, "bedrock-add-role", codersdk.AIProviderBedrockSettings{Region: "us-east-1"}) + require.NoError(t, err) + require.Empty(t, created.Settings.Bedrock.ExternalID) + + updated, err := client.UpdateAIProvider(ctx, created.Name, codersdk.UpdateAIProviderRequest{ + Settings: &codersdk.AIProviderSettings{ + Bedrock: &codersdk.AIProviderBedrockSettings{Region: "us-east-1", RoleARN: roleARN}, + }, + }) + require.NoError(t, err) + require.NotEmpty(t, updated.Settings.Bedrock.ExternalID, "external ID must be generated when a role is added by PATCH") + }) + + t.Run("RejectsClientValueWhenRoleAddedByPatch", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + ctx := testutil.Context(t, testutil.WaitLong) + + created, err := createBedrock(t, client, "bedrock-add-role-id", codersdk.AIProviderBedrockSettings{Region: "us-east-1"}) + require.NoError(t, err) + require.Empty(t, created.Settings.Bedrock.ExternalID) + + // No value is stored yet, so any client value is a change and is rejected. + _, err = client.UpdateAIProvider(ctx, created.Name, codersdk.UpdateAIProviderRequest{ + Settings: &codersdk.AIProviderSettings{ + Bedrock: &codersdk.AIProviderBedrockSettings{Region: "us-east-1", RoleARN: roleARN, ExternalID: "client-supplied-value"}, + }, + }) + sdkErr := requireSDKError(t, err, http.StatusBadRequest) + require.Equal(t, externalIDReadOnlyMsg, sdkErr.Message) + }) +} diff --git a/coderd/aibridged/proto/aibridged.pb.go b/coderd/aibridged/proto/aibridged.pb.go index f503aaa8fb..b4be124d97 100644 --- a/coderd/aibridged/proto/aibridged.pb.go +++ b/coderd/aibridged/proto/aibridged.pb.go @@ -1454,6 +1454,7 @@ type AIProviderKindBedrock struct { Model string `protobuf:"bytes,4,opt,name=model,proto3" json:"model,omitempty"` SmallFastModel string `protobuf:"bytes,5,opt,name=small_fast_model,json=smallFastModel,proto3" json:"small_fast_model,omitempty"` RoleArn string `protobuf:"bytes,6,opt,name=role_arn,json=roleArn,proto3" json:"role_arn,omitempty"` + ExternalId string `protobuf:"bytes,7,opt,name=external_id,json=externalId,proto3" json:"external_id,omitempty"` } func (x *AIProviderKindBedrock) Reset() { @@ -1530,6 +1531,13 @@ func (x *AIProviderKindBedrock) GetRoleArn() string { return "" } +func (x *AIProviderKindBedrock) GetExternalId() string { + if x != nil { + return x.ExternalId + } + return "" +} + var File_coderd_aibridged_proto_aibridged_proto protoreflect.FileDescriptor var file_coderd_aibridged_proto_aibridged_proto_rawDesc = []byte{ @@ -1802,7 +1810,7 @@ var file_coderd_aibridged_proto_aibridged_proto_rawDesc = []byte{ 0x6b, 0x65, 0x79, 0x73, 0x12, 0x36, 0x0a, 0x07, 0x62, 0x65, 0x64, 0x72, 0x6f, 0x63, 0x6b, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4b, 0x69, 0x6e, 0x64, 0x42, 0x65, 0x64, 0x72, - 0x6f, 0x63, 0x6b, 0x52, 0x07, 0x62, 0x65, 0x64, 0x72, 0x6f, 0x63, 0x6b, 0x22, 0xd5, 0x01, 0x0a, + 0x6f, 0x63, 0x6b, 0x52, 0x07, 0x62, 0x65, 0x64, 0x72, 0x6f, 0x63, 0x6b, 0x22, 0xf6, 0x01, 0x0a, 0x15, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4b, 0x69, 0x6e, 0x64, 0x42, 0x65, 0x64, 0x72, 0x6f, 0x63, 0x6b, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x12, 0x1d, @@ -1816,72 +1824,74 @@ var file_coderd_aibridged_proto_aibridged_proto_rawDesc = []byte{ 0x64, 0x65, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x6d, 0x61, 0x6c, 0x6c, 0x46, 0x61, 0x73, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x19, 0x0a, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x61, 0x72, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x72, 0x6f, 0x6c, - 0x65, 0x41, 0x72, 0x6e, 0x32, 0xa9, 0x04, 0x0a, 0x08, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x65, - 0x72, 0x12, 0x59, 0x0a, 0x12, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, - 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x68, 0x0a, 0x17, - 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x12, 0x25, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, - 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x10, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, - 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, - 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x11, 0x52, - 0x65, 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, - 0x12, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x50, - 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, - 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x50, 0x0a, 0x0f, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6f, - 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, + 0x65, 0x41, 0x72, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x78, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x49, 0x64, 0x32, 0xa9, 0x04, 0x0a, 0x08, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, + 0x65, 0x72, 0x12, 0x59, 0x0a, 0x12, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, + 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x68, 0x0a, + 0x17, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x12, 0x25, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x26, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, + 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x10, 0x52, 0x65, 0x63, 0x6f, 0x72, + 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1e, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, + 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, + 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x11, + 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67, + 0x65, 0x12, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, + 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, + 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x50, 0x0a, 0x0f, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, + 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, - 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x59, 0x0a, 0x12, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4d, - 0x6f, 0x64, 0x65, 0x6c, 0x54, 0x68, 0x6f, 0x75, 0x67, 0x68, 0x74, 0x12, 0x20, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x54, - 0x68, 0x6f, 0x75, 0x67, 0x68, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4d, 0x6f, 0x64, 0x65, - 0x6c, 0x54, 0x68, 0x6f, 0x75, 0x67, 0x68, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x32, 0xeb, 0x01, 0x0a, 0x0f, 0x4d, 0x43, 0x50, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, - 0x61, 0x74, 0x6f, 0x72, 0x12, 0x5c, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x12, 0x21, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, - 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x7a, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61, - 0x74, 0x63, 0x68, 0x12, 0x2b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, - 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x2c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, - 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x55, - 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x72, 0x12, 0x47, 0x0a, 0x0c, - 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x12, 0x1a, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, - 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2e, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x65, 0x0a, 0x14, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, - 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x4d, 0x0a, - 0x0e, 0x47, 0x65, 0x74, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, - 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x49, 0x50, 0x72, 0x6f, - 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x32, 0x5a, 0x30, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x64, - 0x2f, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x59, 0x0a, 0x12, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, + 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x54, 0x68, 0x6f, 0x75, 0x67, 0x68, 0x74, 0x12, 0x20, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4d, 0x6f, 0x64, 0x65, 0x6c, + 0x54, 0x68, 0x6f, 0x75, 0x67, 0x68, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4d, 0x6f, 0x64, + 0x65, 0x6c, 0x54, 0x68, 0x6f, 0x75, 0x67, 0x68, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x32, 0xeb, 0x01, 0x0a, 0x0f, 0x4d, 0x43, 0x50, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, + 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x5c, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x12, 0x21, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x22, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x7a, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, + 0x61, 0x74, 0x63, 0x68, 0x12, 0x2b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, + 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x2c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, + 0x55, 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x72, 0x12, 0x47, 0x0a, + 0x0c, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x12, 0x1a, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x65, 0x0a, 0x14, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x4d, + 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, + 0x12, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x49, 0x50, 0x72, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x32, 0x5a, + 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x64, 0x2f, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/coderd/aibridged/proto/aibridged.proto b/coderd/aibridged/proto/aibridged.proto index 1d22ccdaa8..8dc78e7a89 100644 --- a/coderd/aibridged/proto/aibridged.proto +++ b/coderd/aibridged/proto/aibridged.proto @@ -193,4 +193,5 @@ message AIProviderKindBedrock { string model = 4; string small_fast_model = 5; string role_arn = 6; + string external_id = 7; } diff --git a/coderd/aibridgedserver/aibridgedserver.go b/coderd/aibridgedserver/aibridgedserver.go index 78f11cd9cc..8b539153e9 100644 --- a/coderd/aibridgedserver/aibridgedserver.go +++ b/coderd/aibridgedserver/aibridgedserver.go @@ -883,6 +883,7 @@ func aiProviderToProto(row database.AIProvider, keys []database.AIProviderKey) ( Model: settings.Bedrock.Model, SmallFastModel: settings.Bedrock.SmallFastModel, RoleArn: settings.Bedrock.RoleARN, + ExternalId: settings.Bedrock.ExternalID, } } diff --git a/codersdk/aiproviders.go b/codersdk/aiproviders.go index 5632ff5684..7826a921f2 100644 --- a/codersdk/aiproviders.go +++ b/codersdk/aiproviders.go @@ -275,6 +275,12 @@ func (req CreateAIProviderRequest) Validate() []ValidationError { } if req.Settings.Bedrock != nil { validations = append(validations, validateAIProviderRoleARN(req.Settings.Bedrock.RoleARN)...) + if req.Settings.Bedrock.ExternalID != "" { + validations = append(validations, ValidationError{ + Field: "settings.external_id", + Detail: "external_id is server-generated and cannot be set", + }) + } } if req.Type == AIProviderTypeCopilot && len(req.APIKeys) > 0 { validations = append(validations, ValidationError{ diff --git a/codersdk/aiproviders_bedrock.go b/codersdk/aiproviders_bedrock.go index 360b763333..be17f6c95d 100644 --- a/codersdk/aiproviders_bedrock.go +++ b/codersdk/aiproviders_bedrock.go @@ -35,6 +35,11 @@ type AIProviderBedrockSettings struct { // IRSA / EKS Pod Identity / EC2 Instance Profile) signs the AssumeRole // call, and the resulting temporary credentials sign Bedrock requests. RoleARN string `json:"role_arn,omitempty"` + // ExternalID is the STS external ID sent on the AssumeRole call when + // RoleARN is set. The server generates and owns it: create and update + // reject any client-supplied value that differs from the stored one (an + // update may echo the stored value back). + ExternalID string `json:"external_id,omitempty"` } // IsConfigured reports whether any load-bearing Bedrock field is set, diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index b7723777f1..09e6ac16cb 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -335,6 +335,13 @@ export interface AIProviderBedrockSettings { * call, and the resulting temporary credentials sign Bedrock requests. */ readonly role_arn?: string; + /** + * ExternalID is the STS external ID sent on the AssumeRole call when + * RoleARN is set. The server generates and owns it: create and update + * reject any client-supplied value that differs from the stored one (an + * update may echo the stored value back). + */ + readonly external_id?: string; } // From codersdk/aiproviders_bedrock.go