From 9221aec6fa3efb665667bf1accb3bf279f4065a1 Mon Sep 17 00:00:00 2001 From: Salman Siddiqui Date: Tue, 2 Dec 2025 23:10:10 +0530 Subject: [PATCH] Add read only support for event streams --- docs/resources/event_stream.md | 85 ++++++++++++++++++- .../resources/auth0_event_stream/resource.tf | 28 ++++++ internal/auth0/eventstream/expand.go | 22 ++++- internal/auth0/eventstream/flatten.go | 17 +++- internal/auth0/eventstream/resource.go | 84 +++++++++++++++++- internal/auth0/eventstream/resource_test.go | 72 ++++++++++++++++ 6 files changed, 297 insertions(+), 11 deletions(-) diff --git a/docs/resources/event_stream.md b/docs/resources/event_stream.md index e4ffb754d..f9b7e70e1 100644 --- a/docs/resources/event_stream.md +++ b/docs/resources/event_stream.md @@ -44,6 +44,26 @@ resource "auth0_event_stream" "my_event_stream_webhook" { } } } + +# Creates an event stream of type webhook with write-only token +resource "auth0_event_stream" "my_event_stream_webhook_wo" { + name = "my-webhook-wo" + destination_type = "webhook" + subscriptions = [ + "user.created", + "user.updated" + ] + + webhook_configuration { + webhook_endpoint = "https://eof28wtn4v4506o.m.pipedream.net" + + webhook_authorization { + method = "bearer" + token_wo = var.webhook_token # From sensitive variable + token_wo_version = 1 + } + } +} ``` @@ -98,9 +118,72 @@ Required: Optional: - `password` (String, Sensitive) The password for `basic` authentication. Required when `method` is set to `basic`. -- `token` (String, Sensitive) The token used for `bearer` authentication. Required when `method` is set to `bearer`. +- `token` (String, Sensitive) The token used for `bearer` authentication. Required when `method` is set to `bearer`. **Note**: This value is stored in Terraform state. For enhanced security, consider using `token_wo` instead. +- `token_wo` (String, Sensitive, Write-Only) The token used for `bearer` authentication (write-only). This value is not stored in Terraform state and provides enhanced security. Required when `method` is set to `bearer` and `token` is not provided. Must be used together with `token_wo_version`. +- `token_wo_version` (Number) Version number for the write-only token. Increment this value when the token changes to trigger an update. Required when `token_wo` is provided. - `username` (String) The username for `basic` authentication. Required when `method` is set to `basic`. +## Write-Only Token Support + +The `token_wo` field allows you to provide a bearer token without storing it in Terraform state, providing enhanced security for sensitive tokens. + +### Key Features + +- **Not stored in state**: The `token_wo` value is never stored in Terraform state files +- **Not in plan files**: Write-only values are excluded from plan files +- **Version tracking**: Use `token_wo_version` to track changes and trigger updates + +### Usage + +When using `token_wo`, you must also provide `token_wo_version`. To update the token: + +1. Change the `token_wo` value +2. Increment `token_wo_version` (e.g., from `1` to `2`) +3. Run `terraform apply` + +### Example: Updating a Write-Only Token + +```terraform +# Initial creation +resource "auth0_event_stream" "my_event_stream_webhook_wo" { + name = "my-webhook-wo" + destination_type = "webhook" + subscriptions = ["user.created", "user.updated"] + + webhook_configuration { + webhook_endpoint = "https://example.com/webhook" + webhook_authorization { + method = "bearer" + token_wo = var.webhook_token + token_wo_version = 1 + } + } +} + +# When token needs to be updated +resource "auth0_event_stream" "my_event_stream_webhook_wo" { + name = "my-webhook-wo" + destination_type = "webhook" + subscriptions = ["user.created", "user.updated"] + + webhook_configuration { + webhook_endpoint = "https://example.com/webhook" + webhook_authorization { + method = "bearer" + token_wo = var.webhook_token # New token value + token_wo_version = 2 # Incremented to trigger update + } + } +} +``` + +### Important Notes + +- `token_wo` and `token` are mutually exclusive - you cannot use both at the same time +- When `token_wo` is provided, `token_wo_version` is required +- The `token_wo` value cannot be retrieved after resource creation +- Always increment `token_wo_version` when the token changes to ensure Terraform detects the update + ## Import Import is supported using the following syntax: diff --git a/examples/resources/auth0_event_stream/resource.tf b/examples/resources/auth0_event_stream/resource.tf index 24ff4061a..1aded488f 100644 --- a/examples/resources/auth0_event_stream/resource.tf +++ b/examples/resources/auth0_event_stream/resource.tf @@ -31,3 +31,31 @@ resource "auth0_event_stream" "my_event_stream_webhook" { } } } + +# Creates an event stream of type webhook with write-only token +# This example shows how to use write-only token for enhanced security +# The token value is not stored in Terraform state +variable "webhook_token" { + description = "The webhook token (sensitive, write-only)" + type = string + sensitive = true +} + +resource "auth0_event_stream" "my_event_stream_webhook_wo" { + name = "my-webhook-wo" + destination_type = "webhook" + subscriptions = [ + "user.created", + "user.updated" + ] + + webhook_configuration { + webhook_endpoint = "https://eof28wtn4v4506o.m.pipedream.net" + + webhook_authorization { + method = "bearer" + token_wo = var.webhook_token + token_wo_version = 1 + } + } +} diff --git a/internal/auth0/eventstream/expand.go b/internal/auth0/eventstream/expand.go index fd956230a..8d0e8508b 100644 --- a/internal/auth0/eventstream/expand.go +++ b/internal/auth0/eventstream/expand.go @@ -68,8 +68,26 @@ func expandEventStreamDestination(data *schema.ResourceData) *management.EventSt authMap["password"] = v } } else if method == "bearer" { - if v, ok := auth["token"].(string); ok { - authMap["token"] = v + // For write-only token, read from raw config to ensure we get the value + // even if it's not in state (since write-only fields are not stored) + cfg := data.GetRawConfig() + webhookCfgRaw := cfg.GetAttr("webhook_configuration") + if !webhookCfgRaw.IsNull() && webhookCfgRaw.LengthInt() > 0 { + authRaw := webhookCfgRaw.Index(cty.NumberIntVal(0)).GetAttr("webhook_authorization") + if !authRaw.IsNull() && authRaw.LengthInt() > 0 { + tokenWORaw := authRaw.Index(cty.NumberIntVal(0)).GetAttr("token_wo") + if !tokenWORaw.IsNull() { + if tokenWO := value.String(tokenWORaw); tokenWO != nil && *tokenWO != "" { + authMap["token"] = *tokenWO + } + } + } + } + // Fallback to regular token if write-only token is not provided + if _, hasToken := authMap["token"]; !hasToken { + if v, ok := auth["token"].(string); ok && v != "" { + authMap["token"] = v + } } } diff --git a/internal/auth0/eventstream/flatten.go b/internal/auth0/eventstream/flatten.go index ae20377a3..13f739b78 100644 --- a/internal/auth0/eventstream/flatten.go +++ b/internal/auth0/eventstream/flatten.go @@ -80,14 +80,23 @@ func flattenEventStreamDestination(data *schema.ResourceData, dest *management.E if auth["method"] == "basic" { authMap["username"] = auth["username"] - // Token is not returned from the API, so we get it from config if available. + // Password is not returned from the API, so we get it from config if available. if p := data.Get("webhook_configuration.0.webhook_authorization.0.password"); p != nil { authMap["password"] = p } } else if auth["method"] == "bearer" { - // Token is not returned from the API, so we get it from config if available. - if t := data.Get("webhook_configuration.0.webhook_authorization.0.token"); t != nil { - authMap["token"] = t + // For write-only token: preserve version from state, but never read token_wo value + // token_wo is write-only, so it won't be in state after first read + // We check if token_wo_version exists to determine if write-only token is being used + if version := data.Get("webhook_configuration.0.webhook_authorization.0.token_wo_version"); version != nil { + authMap["token_wo_version"] = version + // Do not set token_wo - it's write-only and should never be in state + } else { + // For backward compatibility: preserve regular token from config if it exists + // (only when write-only token is not being used) + if t := data.Get("webhook_configuration.0.webhook_authorization.0.token"); t != nil { + authMap["token"] = t + } } } diff --git a/internal/auth0/eventstream/resource.go b/internal/auth0/eventstream/resource.go index 5de346b6f..85685e977 100644 --- a/internal/auth0/eventstream/resource.go +++ b/internal/auth0/eventstream/resource.go @@ -2,13 +2,16 @@ package eventstream import ( "context" + "fmt" + "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/auth0/terraform-provider-auth0/internal/config" internalError "github.com/auth0/terraform-provider-auth0/internal/error" + "github.com/auth0/terraform-provider-auth0/internal/value" ) var webhookConfig = &schema.Resource{ @@ -47,10 +50,31 @@ var webhookConfig = &schema.Resource{ Description: "The password for `basic` authentication. Required when `method` is set to `basic`.", }, "token": { - Type: schema.TypeString, - Optional: true, - Sensitive: true, - Description: "The token used for `bearer` authentication. Required when `method` is set to `bearer`.", + Type: schema.TypeString, + Optional: true, + Sensitive: true, + Description: "The token used for `bearer` authentication. Required when `method` is set to `bearer`. " + + "**Note**: This value is stored in Terraform state. For enhanced security, consider using `token_wo` instead.", + ConflictsWith: []string{"webhook_configuration.0.webhook_authorization.0.token_wo"}, + }, + "token_wo": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + WriteOnly: true, + Description: "The token used for `bearer` authentication (write-only). " + + "This value is not stored in Terraform state and provides enhanced security. " + + "Required when `method` is set to `bearer` and `token` is not provided. " + + "Must be used together with `token_wo_version`.", + ConflictsWith: []string{"webhook_configuration.0.webhook_authorization.0.token"}, + }, + "token_wo_version": { + Type: schema.TypeInt, + Optional: true, + Description: "Version number for the write-only token. " + + "Increment this value when the token changes to trigger an update. " + + "Required when `token_wo` is provided.", + RequiredWith: []string{"webhook_configuration.0.webhook_authorization.0.token_wo"}, }, }, }, @@ -84,6 +108,7 @@ func NewResource() *schema.Resource { ReadContext: readEventStream, UpdateContext: updateEventStream, DeleteContext: deleteEventStream, + CustomizeDiff: validateWebhookAuthorization, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, @@ -192,3 +217,54 @@ func deleteEventStream(ctx context.Context, data *schema.ResourceData, m interfa return nil } + +// validateWebhookAuthorization validates webhook authorization configuration. +// Ensures that when method is "bearer", either token or token_wo is provided (but not both). +func validateWebhookAuthorization(ctx context.Context, diff *schema.ResourceDiff, m interface{}) error { + webhookCfgList, ok := diff.Get("webhook_configuration").([]interface{}) + if !ok || len(webhookCfgList) == 0 { + return nil + } + + webhookCfg := webhookCfgList[0].(map[string]interface{}) + authList, ok := webhookCfg["webhook_authorization"].([]interface{}) + if !ok || len(authList) == 0 { + return nil + } + + auth := authList[0].(map[string]interface{}) + method, ok := auth["method"].(string) + if !ok || method != "bearer" { + return nil + } + + // Check if both token and token_wo are provided + hasToken := false + hasTokenWO := false + + if token, ok := auth["token"].(string); ok && token != "" { + hasToken = true + } + + // For write-only fields, we need to check the raw config since they're not in state + cfg := diff.GetRawConfig() + webhookCfgRaw := cfg.GetAttr("webhook_configuration") + if !webhookCfgRaw.IsNull() && webhookCfgRaw.LengthInt() > 0 { + authRaw := webhookCfgRaw.Index(cty.NumberIntVal(0)).GetAttr("webhook_authorization") + if !authRaw.IsNull() && authRaw.LengthInt() > 0 { + tokenWORaw := authRaw.Index(cty.NumberIntVal(0)).GetAttr("token_wo") + if !tokenWORaw.IsNull() { + if tokenWO := value.String(tokenWORaw); tokenWO != nil && *tokenWO != "" { + hasTokenWO = true + } + } + } + } + + // Ensure at least one token is provided + if !hasToken && !hasTokenWO { + return fmt.Errorf("when `method` is `bearer`, either `token` or `token_wo` must be provided") + } + + return nil +} diff --git a/internal/auth0/eventstream/resource_test.go b/internal/auth0/eventstream/resource_test.go index 97ecc3fc9..7a62f9207 100644 --- a/internal/auth0/eventstream/resource_test.go +++ b/internal/auth0/eventstream/resource_test.go @@ -132,3 +132,75 @@ func TestAccEventStream(t *testing.T) { }, }) } + +const testEventStreamCreateWebhookWithWriteOnlyToken = ` +resource "auth0_event_stream" "my_event_stream_webhook_wo" { + name = "{{.testName}}-my-webhook-wo" + destination_type = "webhook" + subscriptions = ["user.created", "user.updated"] + + webhook_configuration { + webhook_endpoint = "https://eof28wtn4v4506o.m.pipedream.net" + webhook_authorization { + method = "bearer" + token_wo = "initial-write-only-token" + token_wo_version = 1 + } + } +} +` + +const testEventStreamUpdateWebhookWithWriteOnlyToken = ` +resource "auth0_event_stream" "my_event_stream_webhook_wo" { + name = "{{.testName}}-my-webhook-wo" + destination_type = "webhook" + subscriptions = ["user.updated"] + + webhook_configuration { + webhook_endpoint = "https://eof28wtn4v4506o.m.pipedream.net" + webhook_authorization { + method = "bearer" + token_wo = "updated-write-only-token" + token_wo_version = 2 + } + } +} +` + +func TestAccEventStreamWithWriteOnlyToken(t *testing.T) { + acctest.Test(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: acctest.ParseTestName(testEventStreamCreateWebhookWithWriteOnlyToken, t.Name()), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("auth0_event_stream.my_event_stream_webhook_wo", "name", fmt.Sprintf("%s-my-webhook-wo", t.Name())), + resource.TestCheckResourceAttr("auth0_event_stream.my_event_stream_webhook_wo", "destination_type", "webhook"), + resource.TestCheckResourceAttr("auth0_event_stream.my_event_stream_webhook_wo", "webhook_configuration.0.webhook_endpoint", "https://eof28wtn4v4506o.m.pipedream.net"), + resource.TestCheckResourceAttr("auth0_event_stream.my_event_stream_webhook_wo", "webhook_configuration.0.webhook_authorization.0.method", "bearer"), + // Verify token_wo is NOT in state (write-only) + resource.TestCheckNoResourceAttr("auth0_event_stream.my_event_stream_webhook_wo", "webhook_configuration.0.webhook_authorization.0.token_wo"), + // Verify token_wo_version IS in state + resource.TestCheckResourceAttr("auth0_event_stream.my_event_stream_webhook_wo", "webhook_configuration.0.webhook_authorization.0.token_wo_version", "1"), + resource.TestCheckResourceAttr("auth0_event_stream.my_event_stream_webhook_wo", "subscriptions.#", "2"), + resource.TestCheckTypeSetElemAttr("auth0_event_stream.my_event_stream_webhook_wo", "subscriptions.*", "user.created"), + resource.TestCheckTypeSetElemAttr("auth0_event_stream.my_event_stream_webhook_wo", "subscriptions.*", "user.updated"), + ), + }, + // Update with new token and incremented version + { + Config: acctest.ParseTestName(testEventStreamUpdateWebhookWithWriteOnlyToken, t.Name()), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("auth0_event_stream.my_event_stream_webhook_wo", "name", fmt.Sprintf("%s-my-webhook-wo", t.Name())), + resource.TestCheckResourceAttr("auth0_event_stream.my_event_stream_webhook_wo", "destination_type", "webhook"), + resource.TestCheckResourceAttr("auth0_event_stream.my_event_stream_webhook_wo", "webhook_configuration.0.webhook_authorization.0.method", "bearer"), + // Verify token_wo is still NOT in state + resource.TestCheckNoResourceAttr("auth0_event_stream.my_event_stream_webhook_wo", "webhook_configuration.0.webhook_authorization.0.token_wo"), + // Verify token_wo_version was updated + resource.TestCheckResourceAttr("auth0_event_stream.my_event_stream_webhook_wo", "webhook_configuration.0.webhook_authorization.0.token_wo_version", "2"), + resource.TestCheckResourceAttr("auth0_event_stream.my_event_stream_webhook_wo", "subscriptions.#", "1"), + resource.TestCheckTypeSetElemAttr("auth0_event_stream.my_event_stream_webhook_wo", "subscriptions.*", "user.updated"), + ), + }, + }, + }) +}