limit thumbnails a user can access to his own.

This commit is contained in:
David Christofas
2020-05-27 13:17:10 +02:00
parent 6763455741
commit 4ae1a64376
9 changed files with 163 additions and 54 deletions

View File

@@ -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
go.mod
View File

@@ -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
)

2
go.sum
View File

@@ -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=

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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:
// <filestorage-root>/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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}