Repository navigation
feat: add Anthropic integration resource and data source [ENG-8433] - #257
Conversation
There was a problem hiding this comment.
3 issues found across 17 files
Confidence score: 2/5
internal/api/anthropic_integration.gocan send the admin key to anhttp://endpoint, exposing it in transit. Reject non-HTTPS endpoints before sending the mutation.internal/api/anthropic_integration.gosends the write-only key in a GraphQL request body that is logged atTF_LOG=DEBUGorTRACE. RedactadminKeyfrom request logs.internal/resources/anthropic_integration.gocan ignore a configuredadmin_key_woafter import when its version is null, leaving the existing key unchanged. Handle the null version so the configured key is sent.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="internal/resources/anthropic_integration.go">
<violation number="1" location="internal/resources/anthropic_integration.go:141">
P2: An imported integration with `admin_key_wo` but no `admin_key_wo_version` never sends the configured key: import leaves both versions null, so this condition passes `nil` to `Upsert`, which preserves the old key. Treat a supplied key as a trigger for the initial post-import update, or require the version whenever a key is configured.</violation>
</file>
<file name="internal/api/anthropic_integration.go">
<violation number="1" location="internal/api/anthropic_integration.go:29">
P2: The new admin key is sent through a client that logs the complete GraphQL request body at `TF_LOG=DEBUG` or `TRACE`, so enabling provider debugging exposes this write-only credential in logs. Redact `adminKey` (and other secret variables) before logging request bodies.</violation>
<violation number="2" location="internal/api/anthropic_integration.go:78">
P1: Custom agent: **Flag Security Vulnerabilities**
This mutation sends `AdminKey` to the provider endpoint, but the shared client accepts `http://` endpoints and does not enforce TLS. Reject non-HTTPS endpoints before sending credentials.</violation>
</file>
Architecture diagram
sequenceDiagram
participant TF as Terraform Core
participant P as Provider (Framework)
participant R as AnthropicIntegration Resource
participant DS as AnthropicIntegration Data Source
participant A as API Client
participant G as GraphQL API
participant S as Stacklet Platform
participant An as Anthropic Admin API
Note over TF,An: Anthropic Integration Management Flow
rect rgb(240, 248, 255)
Note over TF,An: Create Operation
TF->>R: Create(plan, config)
R->>R: Validate admin_key_wo present
alt Key Missing
R-->>TF: Error: "Missing admin_key_wo"
end
R->>A: Upsert(orgID, adminKey)
A->>G: Mutation upsertAnthropicIntegration
G->>S: Validate key with Anthropic
S->>An: Check admin key (401 if invalid)
An-->>S: Auth result
alt Key Valid
S-->>G: integration {id, orgId, configured}
G-->>A: Success response
A-->>R: integration data
R->>R: Update state (configured=true)
R-->>TF: State with id, org_id, configured
else Key Invalid
S-->>G: Problems [{InvalidInput}]
G-->>A: Error response
A->>A: fromProblems() -> apiError
A-->>R: Rejected key error
R-->>TF: Error: "Failed to connect to Anthropic"
end
end
rect rgb(255, 248, 220)
Note over TF,An: Update Operation (Key Rotation)
TF->>R: Update(plan, state, config)
R->>R: Compare admin_key_wo_version
alt Version Changed
R->>R: Extract new key from config
alt Key Missing
R-->>TF: Error: "Missing admin_key_wo"
end
R->>A: Upsert(orgID, newKey)
A->>G: Mutation upsertAnthropicIntegration
G->>S: Validate with Anthropic
S-->>G: Result
alt Success
G-->>A: Updated integration
A-->>R: Success
R-->>TF: Updated state (admin_key_wo_version)
else Failure
A-->>R: Error (previous key preserved)
R-->>TF: Apply error
end
else Version Unchanged
R->>A: Upsert(orgID, no key)
A-->>R: Preserve existing key
R-->>TF: State unchanged
end
end
rect rgb(245, 245, 245)
Note over TF,An: Read/Import Operation
TF->>R: Read(state)
R->>A: Read(orgID)
A->>G: Query anthropicIntegration
G-->>A: {id, orgId, configured}
A-->>R: Integration data
R->>R: Update state (configured only)
R-->>TF: Refreshed state
TF->>DS: Read(org_id)
DS->>A: Read(orgID)
A->>G: Query anthropicIntegration
G-->>A: {id, orgId, configured}
A-->>DS: Integration data
DS-->>TF: Data source attributes
end
rect rgb(255, 240, 240)
Note over TF,An: Delete Operation
TF->>R: Delete(state)
R->>A: Remove(orgID)
A->>G: Mutation removeAnthropicIntegration
G->>S: Delete integration
S-->>G: Problems array
alt No Problems
G-->>A: Success
A-->>R: nil error
R-->>TF: State removed
else Conflict Problem
G-->>A: Conflict typed error
A->>A: fromProblems() -> api.Conflict
A-->>R: Conflict error
R->>R: AddWarning (integration gone, key pending deletion)
R-->>TF: Warning + state removed
else Other Error
A-->>R: General error
R-->>TF: Error (state retained)
end
end
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| Problems []problem | ||
| } `graphql:"upsertAnthropicIntegration(input: $input)"` | ||
| } | ||
| if err := a.c.Mutate(ctx, &mutation, map[string]any{"input": input}); err != nil { |
There was a problem hiding this comment.
P1: Custom agent: Flag Security Vulnerabilities
This mutation sends AdminKey to the provider endpoint, but the shared client accepts http:// endpoints and does not enforce TLS. Reject non-HTTPS endpoints before sending credentials.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/api/anthropic_integration.go, line 78:
<comment>This mutation sends `AdminKey` to the provider endpoint, but the shared client accepts `http://` endpoints and does not enforce TLS. Reject non-HTTPS endpoints before sending credentials.</comment>
<file context>
@@ -0,0 +1,103 @@
+ Problems []problem
+ } `graphql:"upsertAnthropicIntegration(input: $input)"`
+ }
+ if err := a.c.Mutate(ctx, &mutation, map[string]any{"input": input}); err != nil {
+ return nil, err
+ }
</file context>
There was a problem hiding this comment.
Out of scope here: the endpoint comes from the provider configuration, and enforcing https would apply to the whole client, not this resource. Leaving as is.
| // sent when its version says there is one. | ||
| input := api.AnthropicIntegrationInput{ | ||
| OrgID: plan.OrgID.ValueString(), | ||
| AdminKey: credentialToSend(&resp.Diagnostics, !state.AdminKeyWOVersion.Equal(plan.AdminKeyWOVersion), "admin_key_wo", config.AdminKeyWO), |
There was a problem hiding this comment.
P2: An imported integration with admin_key_wo but no admin_key_wo_version never sends the configured key: import leaves both versions null, so this condition passes nil to Upsert, which preserves the old key. Treat a supplied key as a trigger for the initial post-import update, or require the version whenever a key is configured.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/resources/anthropic_integration.go, line 141:
<comment>An imported integration with `admin_key_wo` but no `admin_key_wo_version` never sends the configured key: import leaves both versions null, so this condition passes `nil` to `Upsert`, which preserves the old key. Treat a supplied key as a trigger for the initial post-import update, or require the version whenever a key is configured.</comment>
<file context>
@@ -0,0 +1,180 @@
+ // sent when its version says there is one.
+ input := api.AnthropicIntegrationInput{
+ OrgID: plan.OrgID.ValueString(),
+ AdminKey: credentialToSend(&resp.Diagnostics, !state.AdminKeyWOVersion.Equal(plan.AdminKeyWOVersion), "admin_key_wo", config.AdminKeyWO),
+ }
+ if resp.Diagnostics.HasError() {
</file context>
There was a problem hiding this comment.
admin_key_wo now requires admin_key_wo_version, so a key without a version is rejected at plan time. The other resources' write-only attributes get the same treatment separately.
| // that is never returned. | ||
| type AnthropicIntegrationInput struct { | ||
| OrgID string `json:"orgId"` | ||
| AdminKey *string `json:"adminKey,omitempty"` |
There was a problem hiding this comment.
P2: The new admin key is sent through a client that logs the complete GraphQL request body at TF_LOG=DEBUG or TRACE, so enabling provider debugging exposes this write-only credential in logs. Redact adminKey (and other secret variables) before logging request bodies.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/api/anthropic_integration.go, line 29:
<comment>The new admin key is sent through a client that logs the complete GraphQL request body at `TF_LOG=DEBUG` or `TRACE`, so enabling provider debugging exposes this write-only credential in logs. Redact `adminKey` (and other secret variables) before logging request bodies.</comment>
<file context>
@@ -0,0 +1,103 @@
+// that is never returned.
+type AnthropicIntegrationInput struct {
+ OrgID string `json:"orgId"`
+ AdminKey *string `json:"adminKey,omitempty"`
+}
+
</file context>
There was a problem hiding this comment.
Real, and provider-wide: every write-only credential goes through the same logging. Fixed separately in "fix: mask write-only credentials in debug logs", which masks them via the request context; whichever of the two PRs lands second masks this key too.
e902fe4 to
f91bee3
Compare
|
|
||
| func (r *anthropicIntegrationResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { | ||
| resp.Schema = schema.Schema{ | ||
| Description: "Manage an Anthropic integration, which imports the daily cost and usage of one Anthropic organization.\n\n" + |
There was a problem hiding this comment.
this would be a tad more readable as a backticked string with no explicit newlines
|
|
||
| // The API refuses to create an integration without a key; saying so here | ||
| // names the attribute to set. | ||
| if config.AdminKeyWO.IsNull() { |
There was a problem hiding this comment.
Can't we just make the key required?
There was a problem hiding this comment.
Yes, much simpler. admin_key_wo and admin_key_wo_version are both required now, like the Jira profile's, so the create-time check, the validator and their tests are gone.
[ENG-8433](https://stacklet.atlassian.net/browse/ENG-8433) ### what add `stacklet_anthropic_integration` resource and data source, which configure importing an Anthropic organization's cost and usage - `org_id` identifies the integration: changing it replaces the resource, and it's the import ID - `admin_key_wo` / `admin_key_wo_version` are write-only and both required, like the Jira profile's key; `configured` says whether a key is stored - the admin key is masked in debug logs, like the other write-only credentials - a key the API rejects fails the apply with the API's reason - a `Conflict` on remove (the integration is gone but its stored key isn't deleted yet) becomes a warning, since Terraform can't retry once the resource is gone; `fromProblems` now returns a typed `Conflict` - other resources' optional `*_wo` attributes whose version is also optional get an `XXX`: a key without a version is never sent registered as unreleased (`STACKLET_UNRELEASED_FEATURES`) until the platform feature is generally available ### why allow configuring the Anthropic integration through Terraform ### testing acceptance tests for create, import, key rotation, rejected keys (including a failed rotation keeping the stored key), and the data source, recorded against a local platform with a fake Anthropic Admin API; unit test for `fromProblems` ### docs not yet, since it's unreleased
f91bee3 to
ed278eb
Compare
ENG-8433
what
add
stacklet_anthropic_integrationresource and data source, which configure importing an Anthropic organization's cost and usageorg_ididentifies the integration: changing it replaces the resource, and it's the import IDadmin_key_wo/admin_key_wo_versionare write-only and both required, like the Jira profile's key;configuredsays whether a key is storedConflicton remove (the integration is gone but its stored key isn't deleted yet) becomes a warning, since Terraform can't retry once the resource is gone;fromProblemsnow returns a typedConflict*_woattributes whose version is also optional get anXXX: a key without a version is never sentregistered as unreleased (
STACKLET_UNRELEASED_FEATURES) until the platform feature is generally availablewhy
allow configuring the Anthropic integration through Terraform
testing
acceptance tests for create, import, key rotation, rejected keys (including a failed rotation keeping the stored key), and the data source, recorded against a local platform with a fake Anthropic Admin API; unit test for
fromProblemsdocs
not yet, since it's unreleased