Skip to content
This repository has been archived by the owner on Jan 27, 2021. It is now read-only.

limit thumbnails a users can access to his own #26

Merged
merged 1 commit into from
May 27, 2020
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
6 changes: 6 additions & 0 deletions changelog/unreleased/group-thumbnails-by-users.md
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
11 changes: 6 additions & 5 deletions pkg/proto/v0/thumbnails.pb.micro_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
34 changes: 33 additions & 1 deletion pkg/service/v0/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand All @@ -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
}
131 changes: 94 additions & 37 deletions pkg/thumbnail/storage/filesystem.go
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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 filepath.Join(etag[:2], etag[2:4], etag[4:], filename)
}

return key.String()
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:
// <filestorage-root>/users/<3 characters>/<3 characters>/<48 characters>/
// This will balance the folders in setups with many users.
refs marked this conversation as resolved.
Show resolved Hide resolved
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
}
19 changes: 13 additions & 6 deletions pkg/thumbnail/storage/inmemory.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
4 changes: 2 additions & 2 deletions pkg/thumbnail/storage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
5 changes: 3 additions & 2 deletions pkg/thumbnail/thumbnails.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type Request struct {
ImagePath string
Encoder Encoder
ETag string
Username string
}

// Manager is responsible for generating thumbnails
Expand Down Expand Up @@ -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")
}
Expand All @@ -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
}

Expand Down