Repository: fsouza/fake-gcs-server
Pull request #97 (2019-10-16 13:42:59)

Support private acl for objects
allow to set private ACL on objects


Patch:
diff --git a/examples/go/go.mod b/examples/go/go.mod
index 74032db..f84c0c7 100644
--- a/examples/go/go.mod
+++ b/examples/go/go.mod
@@ -3,6 +3,6 @@ module github.com/zigius/fake-gcs-server/examples/go-example
 go 1.12
 
 require (
-	cloud.google.com/go/storage v1.1.1
+	cloud.google.com/go/storage v1.1.0
 	google.golang.org/api v0.11.0
 )
diff --git a/examples/go/go.sum b/examples/go/go.sum
index 062a09c..3d28cf1 100644
--- a/examples/go/go.sum
+++ b/examples/go/go.sum
@@ -14,8 +14,6 @@ cloud.google.com/go/pubsub v1.0.1 h1:W9tAK3E57P75u0XLLR82LZyw8VpAnhmyTOxW9qzmyj8
 cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
 cloud.google.com/go/storage v1.1.0 h1:KYV0dnmEcOuxTd8YLiuQqfx8PzSwDeuDvYGoa5+DbDI=
 cloud.google.com/go/storage v1.1.0/go.mod h1:a81gKs1KmeOyF/qrbeu4APVXICPLcsl0Ilx2XvD7ZYU=
-cloud.google.com/go/storage v1.1.1 h1:ycCxVkVbeNQj8t43giBuzCUJb9g5j1QHua8es8DMb/E=
-cloud.google.com/go/storage v1.1.1/go.mod h1:nbQkUX8zrWh07WKekXr/Phd0q/ERj4IOJnkE+v56Qys=
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
diff --git a/examples/node/package-lock.json b/examples/node/package-lock.json
index fd1138a..72fddab 100644
--- a/examples/node/package-lock.json
+++ b/examples/node/package-lock.json
@@ -40,9 +40,9 @@
       "integrity": "sha512-7WfV4R/3YV5T30WRZW0lqmvZy9hE2/p9MvpI34WuKa2Wz62mLu5XplGTFEMK6uTbJCLWUxTcZ4J4IyClKucE5g=="
     },
     "@google-cloud/storage": {
-      "version": "3.5.0",
-      "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-3.5.0.tgz",
-      "integrity": "sha512-QxJ/zft4Kxbedpu7MQ5ZsNeS5WbonB7H28T32R4hQO2ply/j6n7bXmd5Vz0kzJu/iub20sK/ibgxYoxrgZD6CQ==",
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-3.3.1.tgz",
+      "integrity": "sha512-+OoVaJbETDh9cyDoRGe1MJE2Rhrd6JXjSP7GVpdxCUkwYw8pLll/Oz+84JEB82GebQlxG+GVHGcKYTVcSobB5g==",
       "requires": {
         "@google-cloud/common": "^2.1.1",
         "@google-cloud/paginator": "^2.0.0",
@@ -269,9 +269,9 @@
       }
     },
     "gcs-resumable-upload": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-2.3.0.tgz",
-      "integrity": "sha512-PclXJiEngrVx0c4K0LfE1XOxhmOkBEy39Rrhspdn6jAbbwe4OQMZfjo7Z1LHBrh57+bNZeIN4M+BooYppCoHSg==",
+      "version": "2.2.5",
+      "resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-2.2.5.tgz",
+      "integrity": "sha512-r98Hnxza8oYT21MzpziAB2thz3AURGz54+osWtczxGNxH7Fodb0HVUEtfqTwBS5vcf9RnKwR7c0EMaI8R39feg==",
       "requires": {
         "abort-controller": "^3.0.0",
         "configstore": "^5.0.0",
@@ -282,9 +282,9 @@
       }
     },
     "google-auth-library": {
-      "version": "5.5.0",
-      "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-5.5.0.tgz",
-      "integrity": "sha512-TNeraw4miu6/FhO0jDrHiJuRx3SBrAhxHSPj7+rhif43bKE34UItXX9lejKxo3E8FNytuY4LIVIvpcqMQHSYZw==",
+      "version": "5.4.1",
+      "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-5.4.1.tgz",
+      "integrity": "sha512-0KsKBGvlPBNHBkLmw2dfYWDHTuCGCWJrDTzzbYbJO8/ZfcEQ4RJ9Xo1Tnba0a/ZCxPHUkpHqBMB/aRTvnNvxPg==",
       "requires": {
         "arrify": "^2.0.0",
         "base64-js": "^1.3.0",
diff --git a/fakestorage/object.go b/fakestorage/object.go
index ca646b3..e94e888 100644
--- a/fakestorage/object.go
+++ b/fakestorage/object.go
@@ -12,6 +12,7 @@ import (
 	"strconv"
 	"strings"
 
+	"cloud.google.com/go/storage"
 	"github.com/fsouza/fake-gcs-server/internal/backend"
 	"github.com/gorilla/mux"
 )
@@ -23,8 +24,9 @@ type Object struct {
 	ContentType string `json:"contentType"`
 	Content     []byte `json:"-"`
 	// Crc32c checksum of Content. calculated by server when it's upload methods are used.
-	Crc32c  string `json:"crc32c,omitempty"`
-	Md5Hash string `json:"md5hash,omitempty"`
+	Crc32c  string            `json:"crc32c,omitempty"`
+	Md5Hash string            `json:"md5hash,omitempty"`
+	ACL     storage.ACLEntity `json:"acl,omitempty"`
 }
 
 func (o *Object) id() string {
@@ -102,6 +104,7 @@ func toBackendObjects(objects []Object) []backend.Object {
 			ContentType: o.ContentType,
 			Crc32c:      o.Crc32c,
 			Md5Hash:     o.Md5Hash,
+			ACL:         string(o.ACL),
 		})
 	}
 	return backendObjects
@@ -117,6 +120,7 @@ func fromBackendObjects(objects []backend.Object) []Object {
 			ContentType: o.ContentType,
 			Crc32c:      o.Crc32c,
 			Md5Hash:     o.Md5Hash,
+			ACL:         storage.ACLEntity(o.ACL),
 		})
 	}
 	return backendObjects
@@ -174,6 +178,38 @@ func (s *Server) deleteObject(w http.ResponseWriter, r *http.Request) {
 	w.WriteHeader(http.StatusOK)
 }
 
+func (s *Server) listObjectACL(w http.ResponseWriter, r *http.Request) {
+	vars := mux.Vars(r)
+	obj, err := s.GetObject(vars["bucketName"], vars["objectName"])
+
+	if err != nil {
+		http.Error(w, "not found", http.StatusNotFound)
+		return
+	}
+
+	response := newACLListResponse(obj)
+	json.NewEncoder(w).Encode(response)
+}
+
+func (s *Server) setObjectACL(w http.ResponseWriter, r *http.Request) {
+	vars := mux.Vars(r)
+
+	obj, err := s.GetObject(vars["bucketName"], vars["objectName"])
+	entity := storage.ACLEntity(vars["entity"])
+
+	if err != nil {
+		http.Error(w, "not found", http.StatusNotFound)
+		return
+	}
+
+	obj.ACL = entity
+
+	s.CreateObject(obj)
+
+	response := newACLListResponse(obj)
+	json.NewEncoder(w).Encode(response)
+}
+
 func (s *Server) rewriteObject(w http.ResponseWriter, r *http.Request) {
 	vars := mux.Vars(r)
 	obj, err := s.GetObject(vars["sourceBucket"], vars["sourceObject"])
@@ -189,6 +225,7 @@ func (s *Server) rewriteObject(w http.ResponseWriter, r *http.Request) {
 		Crc32c:      obj.Crc32c,
 		Md5Hash:     obj.Md5Hash,
 		ContentType: obj.ContentType,
+		ACL:         obj.ACL,
 	}
 	s.CreateObject(newObject)
 	w.Header().Set("Content-Type", "application/json")
diff --git a/fakestorage/response.go b/fakestorage/response.go
index ad498a2..13ba8a7 100644
--- a/fakestorage/response.go
+++ b/fakestorage/response.go
@@ -4,7 +4,14 @@
 
 package fakestorage
 
-import "sort"
+import (
+	"net/http"
+	"sort"
+
+	cloudstorage "cloud.google.com/go/storage"
+	"google.golang.org/api/googleapi"
+	"google.golang.org/api/storage/v1"
+)
 
 type listResponse struct {
 	Kind     string        `json:"kind"`
@@ -58,6 +65,7 @@ type objectResponse struct {
 	Size        int64  `json:"size,string"`
 	ContentType string `json:"contentType,omitempty"`
 	Crc32c      string `json:"crc32c,omitempty"`
+	ACL         string `json:"acl,omitempty"`
 	Md5Hash     string `json:"md5hash,omitempty"`
 }
 
@@ -70,10 +78,33 @@ func newObjectResponse(obj Object) objectResponse {
 		Size:        int64(len(obj.Content)),
 		ContentType: obj.ContentType,
 		Crc32c:      obj.Crc32c,
+		ACL:         string(obj.ACL),
 		Md5Hash:     obj.Md5Hash,
 	}
 }
 
+type aclListResponse struct {
+	*storage.ObjectAccessControls
+}
+
+func newACLListResponse(obj Object) aclListResponse {
+	return aclListResponse{
+		&storage.ObjectAccessControls{
+			ServerResponse: googleapi.ServerResponse{
+				HTTPStatusCode: http.StatusOK,
+			},
+			Items: []*storage.ObjectAccessControl{
+				{
+					Bucket: obj.BucketName,
+					Entity: string(obj.ACL),
+					Object: obj.Name,
+					Role:   string(cloudstorage.RoleReader),
+				},
+			},
+		},
+	}
+}
+
 type rewriteResponse struct {
 	Kind                string         `json:"kind"`
 	TotalBytesRewritten int64          `json:"totalBytesRewritten,string"`
diff --git a/fakestorage/server.go b/fakestorage/server.go
index f6bb37c..9825af1 100644
--- a/fakestorage/server.go
+++ b/fakestorage/server.go
@@ -155,6 +155,8 @@ func (s *Server) buildMuxer() {
 	r.Path("/b/{bucketName}").Methods("GET").HandlerFunc(s.getBucket)
 	r.Path("/b/{bucketName}/o").Methods("GET").HandlerFunc(s.listObjects)
 	r.Path("/b/{bucketName}/o").Methods("POST").HandlerFunc(s.insertObject)
+	r.Path("/b/{bucketName}/o/{objectName:.+}/acl").Methods("GET").HandlerFunc(s.listObjectACL)
+	r.Path("/b/{bucketName}/o/{objectName:.+}/acl/{entity}").Methods("PUT").HandlerFunc(s.setObjectACL)
 	r.Path("/b/{bucketName}/o/{objectName:.+}").Methods("GET").HandlerFunc(s.getObject)
 	r.Path("/b/{bucketName}/o/{objectName:.+}").Methods("DELETE").HandlerFunc(s.deleteObject)
 	r.Path("/b/{sourceBucket}/o/{sourceObject:.+}/rewriteTo/b/{destinationBucket}/o/{destinationObject:.+}").HandlerFunc(s.rewriteObject)
diff --git a/internal/backend/object.go b/internal/backend/object.go
index 7b56e89..8f7221e 100644
--- a/internal/backend/object.go
+++ b/internal/backend/object.go
@@ -12,6 +12,7 @@ type Object struct {
 	Content     []byte
 	Crc32c      string
 	Md5Hash     string
+	ACL         string
 }
 
 // ID is useful for comparing objects


Test patch:
diff --git a/fakestorage/object_test.go b/fakestorage/object_test.go
index 42c5b8d..65cde4c 100644
--- a/fakestorage/object_test.go
+++ b/fakestorage/object_test.go
@@ -604,3 +604,42 @@ func TestServerClientObjectDeleteErrors(t *testing.T) {
 		}
 	})
 }
+
+func TestServerClientObjectSetAclPrivate(t *testing.T) {
+	objs := []Object{
+		{BucketName: "some-bucket", Name: "img/public-to-private.jpg"},
+	}
+
+	runServersTest(t, objs, func(t *testing.T, server *Server) {
+		t.Run("public to private", func(t *testing.T) {
+			ctx := context.Background()
+			objHandle := server.Client().Bucket("some-bucket").Object("img/public-to-private.jpg")
+
+			err := objHandle.ACL().Set(ctx, storage.AllAuthenticatedUsers, storage.RoleReader)
+			if err != nil {
+				t.Fatalf("unexpected error while setting acl %+v", err)
+				return
+			}
+
+			rules, err := objHandle.ACL().List(ctx)
+			if err != nil {
+				t.Fatalf("unexpected error while getting acl %+v", err)
+				return
+			}
+
+			if len(rules) != 1 {
+				t.Fatal("acl has no rules")
+				return
+			}
+			if rules[0].Entity != storage.AllAuthenticatedUsers {
+				t.Fatal("acl entity not set to AllAuthenticatedUsers")
+				return
+			}
+
+			if rules[0].Role != storage.RoleReader {
+				t.Fatal("acl role not set to RoleReader")
+				return
+			}
+		})
+	})
+}