mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-06-21 14:29:00 -04:00
limit thumbnails a user can access to his own.
This commit is contained in:
6
changelog/unreleased/group-thumbnails-by-users.md
Normal file
6
changelog/unreleased/group-thumbnails-by-users.md
Normal 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
3
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
|
||||
)
|
||||
|
||||
2
go.sum
2
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=
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user