Skip to content

feat(object): add support encryption sse-c #2845

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/resources/object.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ The following arguments are supported:

* `tags` - (Optional) Map of tags.

* `sse_customer_key` - (Optional) Customer's encryption keys to encrypt data (SSE-C)

* `project_id` - (Defaults to [provider](../index.md#arguments-reference) `project_id`) The ID of the project the bucket is associated with.

~> **Important:** The `project_id` attribute has a particular behavior with s3 products because the s3 API is scoped by project.
Expand Down
436 changes: 218 additions & 218 deletions internal/services/instance/testdata/snapshot-from-object.cassette.yaml

Large diffs are not rendered by default.

67 changes: 62 additions & 5 deletions internal/services/object/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package object
import (
"bytes"
"context"
"crypto/md5" //nolint:gosec
"encoding/base64"
"fmt"
"os"
"strings"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
s3Types "github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/hashicorp/go-cty/cty"
Expand Down Expand Up @@ -105,6 +107,13 @@ func ResourceObject() *schema.Resource {
string(s3Types.ObjectCannedACLPublicRead),
}, false),
},
"sse_customer_key": {
Type: schema.TypeString,
Optional: true,
Sensitive: true,
Description: "Customer's encryption keys to encrypt data (SSE-C)",
ValidateFunc: validation.StringLenBetween(32, 32),
},
"region": regional.Schema(),
"project_id": account.ProjectIDSchema(),
},
Expand Down Expand Up @@ -148,6 +157,16 @@ func resourceObjectCreate(ctx context.Context, d *schema.ResourceData, m interfa
req.ACL = s3Types.ObjectCannedACL(*visibilityStr)
}

if encryptionKeyStr, ok := d.GetOk("sse_customer_key"); ok {
digestMD5, encryption, err := EncryptCustomerKey(encryptionKeyStr.(string))
if err != nil {
return diag.FromErr(err)
}
req.SSECustomerAlgorithm = scw.StringPtr("AES256")
req.SSECustomerKeyMD5 = &digestMD5
req.SSECustomerKey = encryption
}

if filePath, hasFile := d.GetOk("file"); hasFile {
file, err := os.Open(filePath.(string))
if err != nil {
Expand Down Expand Up @@ -192,6 +211,19 @@ func resourceObjectCreate(ctx context.Context, d *schema.ResourceData, m interfa
return resourceObjectRead(ctx, d, m)
}

func EncryptCustomerKey(encryptionKeyStr string) (string, *string, error) {
encryptionKey := []byte(encryptionKeyStr)
h := md5.New() //nolint:gosec
_, err := h.Write(encryptionKey)
if err != nil {
return "", nil, err
}
digest := h.Sum(nil)
digestMD5 := base64.StdEncoding.EncodeToString(digest)
encryption := aws.String(base64.StdEncoding.EncodeToString(encryptionKey))
return digestMD5, encryption, nil
}

func resourceObjectUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
s3Client, region, key, bucket, err := s3ClientWithRegionAndNestedName(ctx, d, m, d.Id())
if err != nil {
Expand All @@ -212,7 +244,15 @@ func resourceObjectUpdate(ctx context.Context, d *schema.ResourceData, m interfa
Metadata: types.ExpandMapStringString(d.Get("metadata")),
ACL: s3Types.ObjectCannedACL(d.Get("visibility").(string)),
}

if encryptionKey, ok := d.GetOk("sse_customer_key"); ok {
digestMD5, encryption, err := EncryptCustomerKey(encryptionKey.(string))
if err != nil {
return diag.FromErr(err)
}
req.SSECustomerAlgorithm = scw.StringPtr("AES256")
req.SSECustomerKeyMD5 = &digestMD5
req.SSECustomerKey = encryption
}
if filePath, hasFile := d.GetOk("file"); hasFile {
file, err := os.Open(filePath.(string))
if err != nil {
Expand All @@ -224,14 +264,24 @@ func resourceObjectUpdate(ctx context.Context, d *schema.ResourceData, m interfa
}
_, err = s3Client.PutObject(ctx, req)
} else {
_, err = s3Client.CopyObject(ctx, &s3.CopyObjectInput{
req := &s3.CopyObjectInput{
Bucket: types.ExpandStringPtr(bucketUpdated),
Key: types.ExpandStringPtr(keyUpdated),
StorageClass: s3Types.StorageClass(d.Get("storage_class").(string)),
CopySource: scw.StringPtr(fmt.Sprintf("%s/%s", bucket, key)),
Metadata: types.ExpandMapStringString(d.Get("metadata")),
ACL: s3Types.ObjectCannedACL(d.Get("visibility").(string)),
})
}
if encryptionKey, ok := d.GetOk("sse_customer_key"); ok {
digestMD5, encryption, err := EncryptCustomerKey(encryptionKey.(string))
if err != nil {
return diag.FromErr(err)
}
req.CopySourceSSECustomerAlgorithm = scw.StringPtr("AES256")
req.CopySourceSSECustomerKeyMD5 = &digestMD5
req.CopySourceSSECustomerKey = encryption
}
_, err = s3Client.CopyObject(ctx, req)
}
if err != nil {
return diag.FromErr(err)
Expand Down Expand Up @@ -274,10 +324,17 @@ func resourceObjectRead(ctx context.Context, d *schema.ResourceData, m interface
ctx, cancel := context.WithTimeout(ctx, d.Timeout(schema.TimeoutRead))
defer cancel()

obj, err := s3Client.HeadObject(ctx, &s3.HeadObjectInput{
req := &s3.HeadObjectInput{
Bucket: types.ExpandStringPtr(bucket),
Key: types.ExpandStringPtr(key),
})
}

if encryption, ok := d.GetOk("sse_customer_key"); ok {
req.SSECustomerKey = aws.String(base64.StdEncoding.EncodeToString([]byte(encryption.(string))))
req.SSECustomerAlgorithm = scw.StringPtr("AES256")
}

obj, err := s3Client.HeadObject(ctx, req)
if err != nil {
return diag.FromErr(err)
}
Expand Down
66 changes: 64 additions & 2 deletions internal/services/object/object_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import (

// // Service information constants
const (
ServiceName = "scw" // Name of service.
EndpointsID = ServiceName // ID to look up a service endpoint with.
ServiceName = "scw" // Name of service.
EndpointsID = ServiceName // ID to look up a service endpoint with.
encryptionStr = "1234567890abcdef1234567890abcdef"
contentToEncypt = "Hello World"
)

func TestAccObject_Basic(t *testing.T) {
Expand Down Expand Up @@ -734,6 +736,66 @@ func TestAccObject_WithBucketName(t *testing.T) {
})
}

func TestAccObject_Encryption(t *testing.T) {
tt := acctest.NewTestTools(t)
defer tt.Cleanup()
bucketName := sdkacctest.RandomWithPrefix("test-acc-scaleway-object-encryption")
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },
ProviderFactories: tt.ProviderFactories,
CheckDestroy: resource.ComposeTestCheckFunc(
objectchecks.IsObjectDestroyed(tt),
objectchecks.IsBucketDestroyed(tt),
),
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(`
resource "scaleway_object_bucket" "base-01" {
name = "%s"
region= "%s"
tags = {
foo = "bar"
}
}

resource scaleway_object "by-content" {
bucket = scaleway_object_bucket.base-01.id
key = "myfile/foo"
content = "Hello World"
sse_customer_key = "%s"
}
`, bucketName, objectTestsMainRegion, encryptionStr),
Check: resource.ComposeTestCheckFunc(
objectchecks.CheckBucketExists(tt, "scaleway_object_bucket.base-01", true),
resource.TestCheckResourceAttr("scaleway_object.by-content", "content", "Hello World"),
),
},
{
Config: fmt.Sprintf(`
resource "scaleway_object_bucket" "base-01" {
name = "%s"
region= "%s"
tags = {
foo = "bar"
}
}

resource scaleway_object "by-content" {
bucket = scaleway_object_bucket.base-01.id
key = "myfile/foo/bar"
content = "Hello World"
sse_customer_key = "%s"
}
`, bucketName, objectTestsMainRegion, encryptionStr),
Check: resource.ComposeTestCheckFunc(
objectchecks.CheckBucketExists(tt, "scaleway_object_bucket.base-01", true),
resource.TestCheckResourceAttr("scaleway_object.by-content", "content", "Hello World"),
),
},
},
})
}

func testAccCheckObjectExists(tt *acctest.TestTools, n string) resource.TestCheckFunc {
return func(state *terraform.State) error {
ctx := context.Background()
Expand Down
Loading
Loading