diff --git a/docs/ephemeral-resources/key_manager_decrypt.md b/docs/ephemeral-resources/key_manager_decrypt.md new file mode 100644 index 000000000..29706b72b --- /dev/null +++ b/docs/ephemeral-resources/key_manager_decrypt.md @@ -0,0 +1,8 @@ +--- +subcategory: "Key Manager" +page_title: "Scaleway: scaleway_key_manager_decrypt" +--- + +# scaleway_key_manager_decrypt (Ephemeral Resource) + + \ No newline at end of file diff --git a/internal/services/keymanager/decrypt_ephemeral_resource.go b/internal/services/keymanager/decrypt_ephemeral_resource.go new file mode 100644 index 000000000..4f1b27661 --- /dev/null +++ b/internal/services/keymanager/decrypt_ephemeral_resource.go @@ -0,0 +1,178 @@ +package keymanager + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + key_manager "github.com/scaleway/scaleway-sdk-go/api/key_manager/v1alpha1" + "github.com/scaleway/scaleway-sdk-go/scw" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/locality" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/locality/regional" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/meta" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/verify" +) + +var ( + _ ephemeral.EphemeralResource = (*DecryptEphemeralResource)(nil) + _ ephemeral.EphemeralResourceWithConfigure = (*DecryptEphemeralResource)(nil) +) + +type DecryptEphemeralResource struct { + keyManagerAPI *key_manager.API + meta *meta.Meta +} + +func NewDecryptEphemeralResource() ephemeral.EphemeralResource { + return &DecryptEphemeralResource{} +} + +func (r *DecryptEphemeralResource) Configure(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + m, ok := req.ProviderData.(*meta.Meta) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Ephemeral Resource Configure Type", + fmt.Sprintf("Expected *meta.Meta, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + client := m.ScwClient() + r.keyManagerAPI = key_manager.NewAPI(client) + r.meta = m +} + +func (r *DecryptEphemeralResource) Metadata(ctx context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_key_manager_decrypt" +} + +type DecryptEphemeralResourceModel struct { + Region types.String `tfsdk:"region"` + KeyID types.String `tfsdk:"key_id"` + Plaintext types.String `tfsdk:"plaintext"` + AssociatedData types.Object `tfsdk:"associated_data"` + // Output + Ciphertext types.String `tfsdk:"ciphertext"` +} + +func (r *DecryptEphemeralResource) Schema(ctx context.Context, req ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "region": regional.SchemaAttribute("Region of the key. If not set, the region is derived from the key_id when possible or from the provider configuration."), + "key_id": schema.StringAttribute{ + Required: true, + Description: "ID of the key to use for decryption. Can be a plain UUID or a regional ID.", + Validators: []validator.String{ + verify.IsStringUUIDOrUUIDWithLocality(), + }, + }, + "ciphertext": schema.StringAttribute{ + Required: true, + Description: "Ciphertext data to decrypt. Data size must be between 1 and 131071 bytes.", + Sensitive: true, + }, + "associated_data": schema.ObjectAttribute{ + Optional: true, + Description: "Must match the associated_data value passed in the encryption request. Only supported by keys with a usage set to `symmetric_encryption`.", + AttributeTypes: map[string]attr.Type{ + "value": types.StringType, + }, + }, + "plaintext": schema.StringAttribute{ + Computed: true, + Description: "Key's decrypted data.", + Sensitive: true, + }, + }, + } +} + +func (r *DecryptEphemeralResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + var data DecryptEphemeralResourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + if r.keyManagerAPI == nil { + resp.Diagnostics.AddError( + "Unconfigured keymanagerAPI", + "The ephemeral resource was not properly configured. The Scaleway client is missing. "+ + "This is usually a bug in the provider. Please report it to the maintainers.", + ) + return + } + + keyID := locality.ExpandID(data.KeyID.ValueString()) + ciphertext := data.Ciphertext.ValueString() + + var region scw.Region + var err error + + if !data.Region.IsNull() && data.Region.ValueString() != "" { + region = scw.Region(data.Region.ValueString()) + } else { + // Try to derive region from the key_id if it is a regional ID + if derivedRegion, id, parseErr := regional.ParseID(keyID); parseErr == nil { + region = derivedRegion + keyID = id + } else { + // Use default region from provider configuration + defaultRegion, exists := r.meta.ScwClient().GetDefaultRegion() + if !exists { + resp.Diagnostics.AddError( + "Missing region", + "The region attribute is required to decrypt with a key. Please provide it explicitly or configure a default region in the provider.", + ) + return + } + region = defaultRegion + } + } + + var associatedData []byte + + if !data.AssociatedData.IsNull() && !data.AssociatedData.IsUnknown() { + var assocDataModel AssociatedDataModel + diags := data.AssociatedData.As(ctx, &assocDataModel, basetypes.ObjectAsOptions{ + UnhandledNullAsEmpty: true, + UnhandledUnknownAsEmpty: true, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + associatedData = []byte(assocDataModel.Value.ValueString()) + } + + decryptReq := &key_manager.DecryptRequest{ + Region: region, + KeyID: keyID, + Ciphertext: []byte(ciphertext), + AssociatedData: &associatedData, + } + + decryptResp, err := r.keyManagerAPI.Decrypt(decryptReq) + if err != nil { + resp.Diagnostics.AddError( + "Error executing Key Manager decrypt action", + fmt.Sprintf("%s", err), + ) + return + } + + data.Plaintext = types.StringValue(string(decryptResp.Plaintext)) + + resp.Result.Set(ctx, &data) +} diff --git a/internal/services/keymanager/decrypt_ephemeral_resource_test.go b/internal/services/keymanager/decrypt_ephemeral_resource_test.go new file mode 100644 index 000000000..c53336b84 --- /dev/null +++ b/internal/services/keymanager/decrypt_ephemeral_resource_test.go @@ -0,0 +1,192 @@ +package keymanager_test + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/acctest" +) + +func TestAccDecryptEphemeralResource_Basic(t *testing.T) { + tt := acctest.NewTestTools(t) + defer tt.Cleanup() + + plainTextData := "this is some secret data" + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: tt.ProviderFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "scaleway_key_manager_key" "main" { + name = "tf-test-decrypt-key" + region = "fr-par" + usage = "symmetric_encryption" + algorithm = "aes_256_gcm" + unprotected = true + } + + ephemeral "scaleway_key_manager_encrypt" "test_encrypt" { + key_id = scaleway_key_manager_key.main.id + plaintext = "%s" + region = "fr-par" + } + + ephemeral "scaleway_key_manager_decrypt" "test_decrypt" { + key_id = scaleway_key_manager_key.main.id + ciphertext = ephemeral.scaleway_key_manager_encrypt.test_encrypt.ciphertext + region = "fr-par" + } + + resource "scaleway_secret" "main" { + name = "test-decrypt-secret" + } + + resource "scaleway_secret_version" "v1" { + description = "test decrypted" + secret_id = scaleway_secret.main.id + data_wo = ephemeral.scaleway_key_manager_decrypt.test_decrypt.plaintext + } + + data "scaleway_secret_version" "v1" { + secret_id = scaleway_secret.main.id + revision = "1" + depends_on = [scaleway_secret_version.v1] + } + `, plainTextData), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.scaleway_secret_version.v1", "data", plainTextData), + ), + }, + }, + }) +} + +func TestAccDecryptEphemeralResource_WithAssociatedData(t *testing.T) { + tt := acctest.NewTestTools(t) + defer tt.Cleanup() + + plainTextData := "this is some secret data" + associatedData := "some associated data" + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: tt.ProviderFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "scaleway_key_manager_key" "test_key" { + name = "tf-test-decrypt-key" + region = "fr-par" + usage = "symmetric_encryption" + algorithm = "aes_256_gcm" + unprotected = true + } + + ephemeral "scaleway_key_manager_encrypt" "test_encrypt" { + key_id = scaleway_key_manager_key.test_key.id + plaintext = "%[1]s" + region = "fr-par" + associated_data = "%[2]s" + } + + ephemeral "scaleway_key_manager_decrypt" "test_decrypt" { + key_id = scaleway_key_manager_key.test_key.id + ciphertext = ephemeral.scaleway_key_manager_encrypt.test_encrypt.ciphertext + region = "fr-par" + associated_data = "%[2]s" + } + + resource "scaleway_secret" "main" { + name = "test-decrypt-secret" + } + + resource "scaleway_secret_version" "data" { + description = "test decrypted data" + secret_id = scaleway_secret.main.id + data_wo = ephemeral.scaleway_key_manager_decrypt.test_decrypt.plaintext + } + + resource "scaleway_secret_version" "associated_data" { + description = "test decrypted associated data" + secret_id = scaleway_secret.main.id + data_wo = ephemeral.scaleway_key_manager_decrypt.test_decrypt.associated_data + } + + data "scaleway_secret_version" "data_v1" { + secret_id = scaleway_secret.main.id + revision = "1" + depends_on = [scaleway_secret_version.data] + } + + data "scaleway_secret_version" "data_v2" { + secret_id = scaleway_secret.main.id + revision = "2" + depends_on = [scaleway_secret_version.associated_data] + } + `, plainTextData, associatedData), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.scaleway_secret_version.data", "data", plainTextData), + resource.TestCheckResourceAttr("data.scaleway_secret_version.associated_data", "data", associatedData), + ), + }, + }, + }) +} + +func TestAccDecryptEphemeralResource_ErrorWrongAssociatedData(t *testing.T) { + tt := acctest.NewTestTools(t) + defer tt.Cleanup() + + plainTextData := "this is some secret data" + associatedData := "some associated data" + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: tt.ProviderFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "scaleway_key_manager_key" "test_key" { + name = "tf-test-decrypt-key" + region = "fr-par" + usage = "symmetric_encryption" + algorithm = "aes_256_gcm" + unprotected = true + } + + ephemeral "scaleway_key_manager_encrypt" "test_encrypt" { + key_id = scaleway_key_manager_key.test_key.id + plaintext = "%s" + region = "fr-par" + associated_data = "%s" + } + + ephemeral "scaleway_key_manager_decrypt" "test_decrypt" { + key_id = scaleway_key_manager_key.test_key.id + ciphertext = ephemeral.scaleway_key_manager_encrypt.test_encrypt.ciphertext + region = "fr-par" + associated_data = "qwerty" + } + + resource "scaleway_secret" "main" { + name = "test-decrypt-secret" + } + + resource "scaleway_secret_version" "data" { + description = "test decrypted data" + secret_id = scaleway_secret.main.id + data = ephemeral.scaleway_key_manager_decrypt.test_decrypt.plaintext + } + + resource "scaleway_secret_version" "associated_data" { + description = "test decrypted associated data" + secret_id = scaleway_secret.main.id + data = ephemeral.scaleway_key_manager_decrypt.test_decrypt.associated_data + } + `, plainTextData, associatedData), + ExpectError: regexp.MustCompile("error"), // TODO: FIX ME + }, + }, + }) +} diff --git a/provider/framework.go b/provider/framework.go index 470058db1..e019a7eec 100644 --- a/provider/framework.go +++ b/provider/framework.go @@ -125,6 +125,7 @@ func (p *ScalewayProvider) Configure(ctx context.Context, req provider.Configure resp.ResourceData = m resp.DataSourceData = m resp.ActionData = m + resp.EphemeralResourceData = m } func (p *ScalewayProvider) Resources(_ context.Context) []func() resource.Resource { @@ -132,7 +133,11 @@ func (p *ScalewayProvider) Resources(_ context.Context) []func() resource.Resour } func (p *ScalewayProvider) EphemeralResources(_ context.Context) []func() ephemeral.EphemeralResource { - return []func() ephemeral.EphemeralResource{} + var res []func() ephemeral.EphemeralResource + + res = append(res, keymanager.NewDecryptEphemeralResource) + + return res } func (p *ScalewayProvider) DataSources(_ context.Context) []func() datasource.DataSource { diff --git a/templates/ephemeral-resource/key_manager_decrypt.md.tmpl b/templates/ephemeral-resource/key_manager_decrypt.md.tmpl new file mode 100644 index 000000000..45d0245c5 --- /dev/null +++ b/templates/ephemeral-resource/key_manager_decrypt.md.tmpl @@ -0,0 +1,9 @@ +{{- /*gotype: github.com/hashicorp/terraform-plugin-docs/internal/provider.ActionTemplateType */ -}} +--- +subcategory: "Key Manager" +page_title: "Scaleway: scaleway_key_manager_decrypt" +--- + +# scaleway_key_manager_decrypt (Ephemeral Resource) + +{{ .SchemaMarkdown }}