Skip to content

Commit 9ed09eb

Browse files
authored
feat(object): add support encryption sse-c (#2845)
* feat(object): support sse-c encryption * feat(object): encryption create is working issue with read * fix(object): copyObject with encryption * update cassette * fix golangci-lint * remove unused data * feat(object): update documentation * add ValidateFunc
1 parent 1c08b7a commit 9ed09eb

File tree

42 files changed

+21009
-17896
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+21009
-17896
lines changed

docs/resources/object.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ The following arguments are supported:
5151

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

54+
* `sse_customer_key` - (Optional) Customer's encryption keys to encrypt data (SSE-C)
55+
5456
* `project_id` - (Defaults to [provider](../index.md#arguments-reference) `project_id`) The ID of the project the bucket is associated with.
5557

5658
~> **Important:** The `project_id` attribute has a particular behavior with s3 products because the s3 API is scoped by project.

internal/services/instance/testdata/snapshot-from-object.cassette.yaml

Lines changed: 218 additions & 218 deletions
Large diffs are not rendered by default.

internal/services/object/object.go

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ package object
33
import (
44
"bytes"
55
"context"
6+
"crypto/md5" //nolint:gosec
67
"encoding/base64"
78
"fmt"
89
"os"
910
"strings"
1011

12+
"github.com/aws/aws-sdk-go-v2/aws"
1113
"github.com/aws/aws-sdk-go-v2/service/s3"
1214
s3Types "github.com/aws/aws-sdk-go-v2/service/s3/types"
1315
"github.com/hashicorp/go-cty/cty"
@@ -105,6 +107,13 @@ func ResourceObject() *schema.Resource {
105107
string(s3Types.ObjectCannedACLPublicRead),
106108
}, false),
107109
},
110+
"sse_customer_key": {
111+
Type: schema.TypeString,
112+
Optional: true,
113+
Sensitive: true,
114+
Description: "Customer's encryption keys to encrypt data (SSE-C)",
115+
ValidateFunc: validation.StringLenBetween(32, 32),
116+
},
108117
"region": regional.Schema(),
109118
"project_id": account.ProjectIDSchema(),
110119
},
@@ -148,6 +157,16 @@ func resourceObjectCreate(ctx context.Context, d *schema.ResourceData, m interfa
148157
req.ACL = s3Types.ObjectCannedACL(*visibilityStr)
149158
}
150159

160+
if encryptionKeyStr, ok := d.GetOk("sse_customer_key"); ok {
161+
digestMD5, encryption, err := EncryptCustomerKey(encryptionKeyStr.(string))
162+
if err != nil {
163+
return diag.FromErr(err)
164+
}
165+
req.SSECustomerAlgorithm = scw.StringPtr("AES256")
166+
req.SSECustomerKeyMD5 = &digestMD5
167+
req.SSECustomerKey = encryption
168+
}
169+
151170
if filePath, hasFile := d.GetOk("file"); hasFile {
152171
file, err := os.Open(filePath.(string))
153172
if err != nil {
@@ -192,6 +211,19 @@ func resourceObjectCreate(ctx context.Context, d *schema.ResourceData, m interfa
192211
return resourceObjectRead(ctx, d, m)
193212
}
194213

214+
func EncryptCustomerKey(encryptionKeyStr string) (string, *string, error) {
215+
encryptionKey := []byte(encryptionKeyStr)
216+
h := md5.New() //nolint:gosec
217+
_, err := h.Write(encryptionKey)
218+
if err != nil {
219+
return "", nil, err
220+
}
221+
digest := h.Sum(nil)
222+
digestMD5 := base64.StdEncoding.EncodeToString(digest)
223+
encryption := aws.String(base64.StdEncoding.EncodeToString(encryptionKey))
224+
return digestMD5, encryption, nil
225+
}
226+
195227
func resourceObjectUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
196228
s3Client, region, key, bucket, err := s3ClientWithRegionAndNestedName(ctx, d, m, d.Id())
197229
if err != nil {
@@ -212,7 +244,15 @@ func resourceObjectUpdate(ctx context.Context, d *schema.ResourceData, m interfa
212244
Metadata: types.ExpandMapStringString(d.Get("metadata")),
213245
ACL: s3Types.ObjectCannedACL(d.Get("visibility").(string)),
214246
}
215-
247+
if encryptionKey, ok := d.GetOk("sse_customer_key"); ok {
248+
digestMD5, encryption, err := EncryptCustomerKey(encryptionKey.(string))
249+
if err != nil {
250+
return diag.FromErr(err)
251+
}
252+
req.SSECustomerAlgorithm = scw.StringPtr("AES256")
253+
req.SSECustomerKeyMD5 = &digestMD5
254+
req.SSECustomerKey = encryption
255+
}
216256
if filePath, hasFile := d.GetOk("file"); hasFile {
217257
file, err := os.Open(filePath.(string))
218258
if err != nil {
@@ -224,14 +264,24 @@ func resourceObjectUpdate(ctx context.Context, d *schema.ResourceData, m interfa
224264
}
225265
_, err = s3Client.PutObject(ctx, req)
226266
} else {
227-
_, err = s3Client.CopyObject(ctx, &s3.CopyObjectInput{
267+
req := &s3.CopyObjectInput{
228268
Bucket: types.ExpandStringPtr(bucketUpdated),
229269
Key: types.ExpandStringPtr(keyUpdated),
230270
StorageClass: s3Types.StorageClass(d.Get("storage_class").(string)),
231271
CopySource: scw.StringPtr(fmt.Sprintf("%s/%s", bucket, key)),
232272
Metadata: types.ExpandMapStringString(d.Get("metadata")),
233273
ACL: s3Types.ObjectCannedACL(d.Get("visibility").(string)),
234-
})
274+
}
275+
if encryptionKey, ok := d.GetOk("sse_customer_key"); ok {
276+
digestMD5, encryption, err := EncryptCustomerKey(encryptionKey.(string))
277+
if err != nil {
278+
return diag.FromErr(err)
279+
}
280+
req.CopySourceSSECustomerAlgorithm = scw.StringPtr("AES256")
281+
req.CopySourceSSECustomerKeyMD5 = &digestMD5
282+
req.CopySourceSSECustomerKey = encryption
283+
}
284+
_, err = s3Client.CopyObject(ctx, req)
235285
}
236286
if err != nil {
237287
return diag.FromErr(err)
@@ -274,10 +324,17 @@ func resourceObjectRead(ctx context.Context, d *schema.ResourceData, m interface
274324
ctx, cancel := context.WithTimeout(ctx, d.Timeout(schema.TimeoutRead))
275325
defer cancel()
276326

277-
obj, err := s3Client.HeadObject(ctx, &s3.HeadObjectInput{
327+
req := &s3.HeadObjectInput{
278328
Bucket: types.ExpandStringPtr(bucket),
279329
Key: types.ExpandStringPtr(key),
280-
})
330+
}
331+
332+
if encryption, ok := d.GetOk("sse_customer_key"); ok {
333+
req.SSECustomerKey = aws.String(base64.StdEncoding.EncodeToString([]byte(encryption.(string))))
334+
req.SSECustomerAlgorithm = scw.StringPtr("AES256")
335+
}
336+
337+
obj, err := s3Client.HeadObject(ctx, req)
281338
if err != nil {
282339
return diag.FromErr(err)
283340
}

internal/services/object/object_test.go

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ import (
2121

2222
// // Service information constants
2323
const (
24-
ServiceName = "scw" // Name of service.
25-
EndpointsID = ServiceName // ID to look up a service endpoint with.
24+
ServiceName = "scw" // Name of service.
25+
EndpointsID = ServiceName // ID to look up a service endpoint with.
26+
encryptionStr = "1234567890abcdef1234567890abcdef"
27+
contentToEncypt = "Hello World"
2628
)
2729

2830
func TestAccObject_Basic(t *testing.T) {
@@ -734,6 +736,66 @@ func TestAccObject_WithBucketName(t *testing.T) {
734736
})
735737
}
736738

739+
func TestAccObject_Encryption(t *testing.T) {
740+
tt := acctest.NewTestTools(t)
741+
defer tt.Cleanup()
742+
bucketName := sdkacctest.RandomWithPrefix("test-acc-scaleway-object-encryption")
743+
resource.ParallelTest(t, resource.TestCase{
744+
PreCheck: func() { acctest.PreCheck(t) },
745+
ProviderFactories: tt.ProviderFactories,
746+
CheckDestroy: resource.ComposeTestCheckFunc(
747+
objectchecks.IsObjectDestroyed(tt),
748+
objectchecks.IsBucketDestroyed(tt),
749+
),
750+
Steps: []resource.TestStep{
751+
{
752+
Config: fmt.Sprintf(`
753+
resource "scaleway_object_bucket" "base-01" {
754+
name = "%s"
755+
region= "%s"
756+
tags = {
757+
foo = "bar"
758+
}
759+
}
760+
761+
resource scaleway_object "by-content" {
762+
bucket = scaleway_object_bucket.base-01.id
763+
key = "myfile/foo"
764+
content = "Hello World"
765+
sse_customer_key = "%s"
766+
}
767+
`, bucketName, objectTestsMainRegion, encryptionStr),
768+
Check: resource.ComposeTestCheckFunc(
769+
objectchecks.CheckBucketExists(tt, "scaleway_object_bucket.base-01", true),
770+
resource.TestCheckResourceAttr("scaleway_object.by-content", "content", "Hello World"),
771+
),
772+
},
773+
{
774+
Config: fmt.Sprintf(`
775+
resource "scaleway_object_bucket" "base-01" {
776+
name = "%s"
777+
region= "%s"
778+
tags = {
779+
foo = "bar"
780+
}
781+
}
782+
783+
resource scaleway_object "by-content" {
784+
bucket = scaleway_object_bucket.base-01.id
785+
key = "myfile/foo/bar"
786+
content = "Hello World"
787+
sse_customer_key = "%s"
788+
}
789+
`, bucketName, objectTestsMainRegion, encryptionStr),
790+
Check: resource.ComposeTestCheckFunc(
791+
objectchecks.CheckBucketExists(tt, "scaleway_object_bucket.base-01", true),
792+
resource.TestCheckResourceAttr("scaleway_object.by-content", "content", "Hello World"),
793+
),
794+
},
795+
},
796+
})
797+
}
798+
737799
func testAccCheckObjectExists(tt *acctest.TestTools, n string) resource.TestCheckFunc {
738800
return func(state *terraform.State) error {
739801
ctx := context.Background()

0 commit comments

Comments
 (0)