package blob import ( "context" "io" "net/http" "os" "strings" "testing" "time" ) // Live tests. Skipped unless the bucket credentials are in the environment, so // the suite stays runnable with no network - the same rule the Python tests // follow about needing no camera and no models. func liveStore(t *testing.T) *Store { t.Helper() cfg := Config{ Region: os.Getenv("DO_SPACES_REGION"), Endpoint: os.Getenv("DO_SPACES_ENDPOINT"), Bucket: os.Getenv("DO_SPACES_BUCKET"), AccessKey: os.Getenv("DO_SPACES_ACCESS_KEY"), SecretKey: os.Getenv("DO_SPACES_SECRET_KEY"), Prefix: "behavision/_test", } if cfg.AccessKey == "" { t.Skip("object storage credentials not in the environment") } s, err := New(cfg) if err != nil { t.Fatal(err) } return s } func anonGet(t *testing.T, url string) int { t.Helper() resp, err := http.Get(url) if err != nil { t.Fatal(err) } defer resp.Body.Close() io.Copy(io.Discard, resp.Body) //nolint:errcheck return resp.StatusCode } // The one that matters. The bucket is world-readable at the bucket level, so // an object written with the wrong ACL is a customer's face downloadable by // anyone who lists the bucket. func TestUploadedImagesAreNotPubliclyReadable(t *testing.T) { s := liveStore(t) ctx := context.Background() key := s.Key("acme", "store1", "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", time.Now()) if err := s.Put(ctx, key, []byte("not really a jpeg"), "image/jpeg"); err != nil { t.Fatal(err) } defer s.Delete(ctx, key) //nolint:errcheck if code := anonGet(t, "https://"+s.host+"/"+key); code != http.StatusForbidden { t.Fatalf("an uploaded image answered %d to an anonymous GET, want 403 - "+ "face images are being published to the internet", code) } url, err := s.PresignGet(key, 5*time.Minute) if err != nil { t.Fatal(err) } if code := anonGet(t, url); code != http.StatusOK { t.Fatalf("presigned read returned %d, want 200", code) } } // Erasure has to actually erase. A deletion that leaves the image in the // bucket has deleted nothing, and this is the operation a DPDP/GDPR request // turns into. func TestDeleteRemovesTheObjectAndIsIdempotent(t *testing.T) { s := liveStore(t) ctx := context.Background() key := s.Key("acme", "store1", "11111111-2222-3333-4444-555555555555", time.Now()) if err := s.Put(ctx, key, []byte("x"), "image/jpeg"); err != nil { t.Fatal(err) } if err := s.Delete(ctx, key); err != nil { t.Fatal(err) } url, _ := s.PresignGet(key, 5*time.Minute) if code := anonGet(t, url); code != http.StatusNotFound { t.Fatalf("presigned read after delete returned %d, want 404", code) } // A retry after a half-finished erasure must be able to finish rather than // failing forever on the object that already went. if err := s.Delete(ctx, key); err != nil { t.Fatalf("deleting an absent object failed: %v", err) } } // The agent PUTs through a presigned URL and holds no bucket credentials. func TestPresignedPutAcceptsOnlyThePrivateACL(t *testing.T) { s := liveStore(t) ctx := context.Background() key := s.Key("acme", "store1", "99999999-8888-7777-6666-555555555555", time.Now()) url, hdr, err := s.PresignPut(key, 5*time.Minute) if err != nil { t.Fatal(err) } defer s.Delete(ctx, key) //nolint:errcheck upload := func(acl string) int { req, _ := http.NewRequest(http.MethodPut, url, strings.NewReader("jpeg bytes")) req.Header.Set("Content-Type", hdr.Get("Content-Type")) req.Header.Set("x-amz-acl", acl) resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() io.Copy(io.Discard, resp.Body) //nolint:errcheck return resp.StatusCode } if code := upload("private"); code != http.StatusOK { t.Fatalf("presigned upload returned %d, want 200", code) } // The ACL is signed into the URL, so an agent cannot decide to publish the // image instead. Without this the shop PC picks the privacy policy. if code := upload("public-read"); code == http.StatusOK { t.Fatal("an agent was able to override the ACL and make the image public") } if code := anonGet(t, "https://"+s.host+"/"+key); code == http.StatusOK { t.Fatal("the uploaded object is publicly readable") } } func TestCheckRefusesABucketThatServesObjectsPublicly(t *testing.T) { s := liveStore(t) if err := s.Check(context.Background()); err != nil { t.Fatalf("self-test failed: %v", err) } }