diff --git a/changelog/unreleased/group-thumbnails-by-users.md b/changelog/unreleased/group-thumbnails-by-users.md new file mode 100644 index 0000000000..2ccb43a229 --- /dev/null +++ b/changelog/unreleased/group-thumbnails-by-users.md @@ -0,0 +1,6 @@ +Enhancement: Limit users to access own thumbnails + +Users of the service can no longer request thumbnails of another users by guessing the etag. +The thumbnails are now only accessible by the users who created the thumbnail. + +https://github.com/owncloud/ocis-thumbnails/issues/5 diff --git a/go.mod b/go.mod index 41606b23eb..3096f178d7 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31 // indirect github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d // indirect github.com/golang/protobuf v1.3.2 + github.com/haya14busa/goverage v0.0.0-20180129164344-eec3514a20b5 // indirect github.com/imdario/mergo v0.3.9 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/mattn/go-colorable v0.1.2 // indirect @@ -23,6 +24,7 @@ require ( github.com/oklog/run v1.0.0 github.com/openzipkin/zipkin-go v0.2.2 github.com/owncloud/ocis-pkg/v2 v2.2.1 + github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.2.1 github.com/restic/calens v0.2.0 github.com/spf13/afero v1.2.2 // indirect @@ -32,5 +34,6 @@ require ( golang.org/x/crypto v0.0.0-20200420201142-3c4aac89819a // indirect golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect golang.org/x/tools v0.0.0-20200421042724-cfa8b22178d2 // indirect + gopkg.in/square/go-jose.v2 v2.3.1 honnef.co/go/tools v0.0.1-2020.1.3 // indirect ) diff --git a/go.sum b/go.sum index 774f1c0ca7..07112a389f 100644 --- a/go.sum +++ b/go.sum @@ -263,6 +263,8 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/haya14busa/goverage v0.0.0-20180129164344-eec3514a20b5 h1:FdBGmSkD2QpQzRWup//SGObvWf2nq89zj9+ta9OvI3A= +github.com/haya14busa/goverage v0.0.0-20180129164344-eec3514a20b5/go.mod h1:0YZ2wQSuwviXXXGUiK6zXzskyBLAbLXhamxzcFHSLoM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0= github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= diff --git a/pkg/proto/v0/thumbnails.pb.micro_test.go b/pkg/proto/v0/thumbnails.pb.micro_test.go index 22dd00571d..4f7ccf556c 100644 --- a/pkg/proto/v0/thumbnails.pb.micro_test.go +++ b/pkg/proto/v0/thumbnails.pb.micro_test.go @@ -66,11 +66,12 @@ func TestGetThumbnailInvalidImage(t *testing.T) { func TestGetThumbnail(t *testing.T) { req := proto.GetRequest{ - Filepath: "oc.png", - Filetype: proto.GetRequest_PNG, - Etag: "33a64df551425fcc55e4d42a148795d9f25f89d4", - Height: 32, - Width: 32, + Filepath: "oc.png", + Filetype: proto.GetRequest_PNG, + Etag: "33a64df551425fcc55e4d42a148795d9f25f89d4", + Height: 32, + Width: 32, + Authorization: "Bearer eyJhbGciOiJQUzI1NiIsImtpZCI6IiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJwaG9lbml4IiwiZXhwIjoxNTkwNTc1Mzk4LCJqdGkiOiJqUEw5c1A3UUEzY0diYi1yRnhkSjJCWnFPc1BDTDg1ZyIsImlhdCI6MTU5MDU3NDc5OCwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6OTIwMCIsInN1YiI6Ilh0U2lfbWl5V1NCLXBrdkdueFBvQzVBNGZsaWgwVUNMZ3ZVN2NMd2ptakNLWDdGWW4ySFdrNnJSQ0V1eTJHNXFBeV95TVFjX0ZLOWFORmhVTXJYMnBRQGtvbm5lY3QiLCJrYy5pc0FjY2Vzc1Rva2VuIjp0cnVlLCJrYy5hdXRob3JpemVkU2NvcGVzIjpbIm9wZW5pZCIsInByb2ZpbGUiLCJlbWFpbCJdLCJrYy5pZGVudGl0eSI6eyJrYy5pLmRuIjoiRWluc3RlaW4iLCJrYy5pLmlkIjoiY249ZWluc3RlaW4sb3U9dXNlcnMsZGM9ZXhhbXBsZSxkYz1vcmciLCJrYy5pLnVuIjoiZWluc3RlaW4ifSwia2MucHJvdmlkZXIiOiJpZGVudGlmaWVyLWxkYXAifQ.FSDe4vzwYpHbNfckBON5EI-01MS_dYFxenddqfJPzjlAEMEH2FFn2xQHCsxhC7wSxivhjV7Z5eRoNUR606keA64Tjs8pJBNECSptBMmE_xfAlc6X5IFILgDnR5bBu6Z2hhu-dVj72Hcyvo_X__OeWekYu7oyoXW41Mw3ayiUAwjCAzV3WPOAJ_r0zbW68_m29BgH3BoSxaF6lmjStIIAIyw7IBZ2QXb_FvGouknmfeWlGL9lkFPGL_dYKwjWieG947nY4Kg8IvHByEbw-xlY3L2EdA7Q8ZMbqdX7GzjtEIVYvCT4-TxWRcmB3SmO-Z8CVq27NHlKm3aZ0k2PS8Ga1w", } client := service.Client() cl := proto.NewThumbnailService("com.owncloud.api.thumbnails", client) diff --git a/pkg/service/v0/service.go b/pkg/service/v0/service.go index 59f144a0eb..8f8d10d220 100644 --- a/pkg/service/v0/service.go +++ b/pkg/service/v0/service.go @@ -4,11 +4,14 @@ import ( "context" "fmt" + "gopkg.in/square/go-jose.v2/jwt" + "github.com/owncloud/ocis-pkg/v2/log" v0proto "github.com/owncloud/ocis-thumbnails/pkg/proto/v0" "github.com/owncloud/ocis-thumbnails/pkg/thumbnail" "github.com/owncloud/ocis-thumbnails/pkg/thumbnail/imgsource" "github.com/owncloud/ocis-thumbnails/pkg/thumbnail/resolution" + "github.com/pkg/errors" ) // NewService returns a service implementation for Service. @@ -47,11 +50,22 @@ func (g Thumbnail) GetThumbnail(ctx context.Context, req *v0proto.GetRequest, rs return fmt.Errorf("can't be encoded. filetype %s not supported", req.Filetype.String()) } r := g.resolutions.ClosestMatch(int(req.Width), int(req.Height)) + + auth := req.Authorization + if auth == "" { + return fmt.Errorf("authorization is missing") + } + username, err := usernameFromAuthorization(auth) + if err != nil { + return err + } + tr := thumbnail.Request{ Resolution: r, ImagePath: req.Filepath, Encoder: encoder, ETag: req.Etag, + Username: username, } thumbnail := g.manager.GetStored(tr) @@ -61,7 +75,6 @@ func (g Thumbnail) GetThumbnail(ctx context.Context, req *v0proto.GetRequest, rs return nil } - auth := req.Authorization sCtx := imgsource.WithAuthorization(ctx, auth) img, err := g.source.Get(sCtx, tr.ImagePath) if err != nil { @@ -79,3 +92,22 @@ func (g Thumbnail) GetThumbnail(ctx context.Context, req *v0proto.GetRequest, rs rsp.Mimetype = tr.Encoder.MimeType() return nil } + +func usernameFromAuthorization(auth string) (string, error) { + tokenString := auth[len("Bearer "):] // strip the bearer prefix + + var claims map[string]interface{} + token, err := jwt.ParseSigned(tokenString) + if err != nil { + return "", errors.Wrap(err, "could not parse auth token") + } + err = token.UnsafeClaimsWithoutVerification(&claims) + if err != nil { + return "", errors.Wrap(err, "could not get claims from auth token") + } + + identityMap := claims["kc.identity"].(map[string]interface{}) + username := identityMap["kc.i.un"].(string) + + return username, nil +} diff --git a/pkg/thumbnail/storage/filesystem.go b/pkg/thumbnail/storage/filesystem.go index 83fbef88e5..7173d813ff 100644 --- a/pkg/thumbnail/storage/filesystem.go +++ b/pkg/thumbnail/storage/filesystem.go @@ -1,64 +1,62 @@ package storage import ( - "bytes" - "fmt" + "crypto/sha256" + "encoding/hex" "io/ioutil" "os" "path/filepath" + "strings" + "sync" "github.com/owncloud/ocis-pkg/v2/log" "github.com/owncloud/ocis-thumbnails/pkg/config" + "github.com/pkg/errors" +) + +const ( + usersDir = "users" + filesDir = "files" ) // NewFileSystemStorage creates a new instanz of FileSystem -func NewFileSystemStorage(cfg config.FileSystemStorage, logger log.Logger) FileSystem { - return FileSystem{ - dir: cfg.RootDirectory, +func NewFileSystemStorage(cfg config.FileSystemStorage, logger log.Logger) *FileSystem { + return &FileSystem{ + root: cfg.RootDirectory, logger: logger, } } // FileSystem represents a storage for the thumbnails using the local file system. type FileSystem struct { - dir string + root string logger log.Logger + mux sync.Mutex } // Get loads the image from the file system. -func (s FileSystem) Get(key string) []byte { - path := filepath.Join(s.dir, key) - content, err := ioutil.ReadFile(filepath.Clean(path)) +func (s *FileSystem) Get(username string, key string) []byte { + userDir := s.userDir(username) + img := filepath.Join(userDir, key) + content, err := ioutil.ReadFile(img) if err != nil { s.logger.Warn().Err(err).Msgf("could not read file %s", key) return nil } - return content } // Set writes the image to the file system. -func (s FileSystem) Set(key string, img []byte) error { - path := filepath.Join(s.dir, key) - dir := filepath.Dir(path) - if err := os.MkdirAll(dir, 0700); err != nil { - return fmt.Errorf("error while creating directory %s", dir) - } - - f, err := os.Create(path) +func (s *FileSystem) Set(username string, key string, img []byte) error { + _, err := s.storeImage(key, img) if err != nil { - return fmt.Errorf("could not create file \"%s\" error: %s", key, err.Error()) + return errors.Wrap(err, "could not store image") } - defer func() { - if err := f.Close(); err != nil { - s.logger.Warn().Err(err).Msg("closing file resulted in an error") - } - }() - _, err = f.Write(img) + userDir, err := s.createUserDir(username) if err != nil { - return fmt.Errorf("could not write to file \"%s\" error: %s", key, err.Error()) + return err } - return nil + return s.linkImageToUserDir(key, userDir) } // BuildKey generate the unique key for a thumbnail. @@ -69,19 +67,78 @@ func (s FileSystem) Set(key string, img []byte) error { // e.g. 97/9f/4c8db98f7b82e768ef478d3c8612/500x300.png // // The key also represents the path to the thumbnail in the filesystem under the configured root directory. -func (s FileSystem) BuildKey(r Request) string { +func (s *FileSystem) BuildKey(r Request) string { etag := r.ETag filetype := r.Types[0] filename := r.Resolution.String() + "." + filetype - key := new(bytes.Buffer) - key.WriteString(etag[:2]) - key.WriteRune('/') - key.WriteString(etag[2:4]) - key.WriteRune('/') - key.WriteString(etag[4:]) - key.WriteRune('/') - key.WriteString(filename) - - return key.String() + return filepath.Join(etag[:2], etag[2:4], etag[4:], filename) +} + +func (s *FileSystem) rootDir(key string) string { + p := strings.Split(key, string(os.PathSeparator)) + return p[0] +} + +func (s *FileSystem) storeImage(key string, img []byte) (string, error) { + s.mux.Lock() + defer s.mux.Unlock() + imgPath := filepath.Join(s.root, filesDir, key) + dir := filepath.Dir(imgPath) + if err := os.MkdirAll(dir, 0700); err != nil { + return "", errors.Wrapf(err, "error while creating directory %s", dir) + } + + if _, err := os.Stat(imgPath); os.IsNotExist(err) { + f, err := os.Create(imgPath) + if err != nil { + return "", errors.Wrapf(err, "could not create file \"%s\"", key) + } + defer f.Close() + + _, err = f.Write(img) + if err != nil { + return "", errors.Wrapf(err, "could not write to file \"%s\"", key) + } + } + + return imgPath, nil +} + +// userDir returns the path to the user directory. +// The username is hashed before appending it on the path to prevent bugs caused by invalid folder names. +// Also the hash is then splitted up in three parts that results in a path which looks as follows: +// /users/<3 characters>/<3 characters>/<48 characters>/ +// This will balance the folders in setups with many users. +func (s *FileSystem) userDir(username string) string { + hash := sha256.New224() + hash.Write([]byte(username)) + unHash := hex.EncodeToString(hash.Sum(nil)) // 224 Bits or 224 / 4 = 56 characters. + + return filepath.Join(s.root, usersDir, unHash[:3], unHash[3:6], unHash[6:]) +} + +func (s *FileSystem) createUserDir(username string) (string, error) { + userDir := s.userDir(username) + if err := os.MkdirAll(userDir, 0700); err != nil { + return "", errors.Wrapf(err, "could not create userDir: %s", userDir) + } + + return userDir, nil +} + +// linkImageToUserDir links the stored images to the user directory. +// The goal is to minimize disk usage by linking to the images if they already exist and avoid file duplicaiton. +func (s *FileSystem) linkImageToUserDir(key string, userDir string) error { + imgRootDir := s.rootDir(key) + + s.mux.Lock() + defer s.mux.Unlock() + err := os.Symlink(filepath.Join(s.root, filesDir, imgRootDir), filepath.Join(userDir, imgRootDir)) + if err != nil { + if !os.IsExist(err) { + return errors.Wrap(err, "could not link image to userdir") + } + } + return nil } diff --git a/pkg/thumbnail/storage/inmemory.go b/pkg/thumbnail/storage/inmemory.go index 12017eed0c..90afe95abd 100644 --- a/pkg/thumbnail/storage/inmemory.go +++ b/pkg/thumbnail/storage/inmemory.go @@ -7,24 +7,31 @@ import ( // NewInMemoryStorage creates a new InMemory instance. func NewInMemoryStorage() InMemory { return InMemory{ - store: make(map[string][]byte), + store: make(map[string]map[string][]byte), } } // InMemory represents an in memory storage for thumbnails // Can be used during development type InMemory struct { - store map[string][]byte + store map[string]map[string][]byte } // Get loads the thumbnail from memory. -func (s InMemory) Get(key string) []byte { - return s.store[key] +func (s InMemory) Get(username string, key string) []byte { + userImages := s.store[username] + if userImages == nil { + return nil + } + return s.store[username][key] } // Set stores the thumbnail in memory. -func (s InMemory) Set(key string, thumbnail []byte) error { - s.store[key] = thumbnail +func (s InMemory) Set(username string, key string, thumbnail []byte) error { + if _, ok := s.store[username]; !ok { + s.store[username] = make(map[string][]byte) + } + s.store[username][key] = thumbnail return nil } diff --git a/pkg/thumbnail/storage/storage.go b/pkg/thumbnail/storage/storage.go index a23a757d3c..55d9682696 100644 --- a/pkg/thumbnail/storage/storage.go +++ b/pkg/thumbnail/storage/storage.go @@ -11,7 +11,7 @@ type Request struct { // Storage defines the interface for a thumbnail store. type Storage interface { - Get(string) []byte - Set(string, []byte) error + Get(string, string) []byte + Set(string, string, []byte) error BuildKey(Request) string } diff --git a/pkg/thumbnail/thumbnails.go b/pkg/thumbnail/thumbnails.go index 846cd60138..d600b53ad6 100644 --- a/pkg/thumbnail/thumbnails.go +++ b/pkg/thumbnail/thumbnails.go @@ -16,6 +16,7 @@ type Request struct { ImagePath string Encoder Encoder ETag string + Username string } // Manager is responsible for generating thumbnails @@ -53,7 +54,7 @@ func (s SimpleManager) Get(r Request, img image.Image) ([]byte, error) { return nil, err } bytes := buf.Bytes() - err = s.storage.Set(key, bytes) + err = s.storage.Set(r.Username, key, bytes) if err != nil { s.logger.Warn().Err(err).Msg("could not store thumbnail") } @@ -64,7 +65,7 @@ func (s SimpleManager) Get(r Request, img image.Image) ([]byte, error) { // If there is no cached thumbnail it will return nil func (s SimpleManager) GetStored(r Request) []byte { key := s.storage.BuildKey(mapToStorageRequest(r)) - stored := s.storage.Get(key) + stored := s.storage.Get(r.Username, key) return stored }