feat: check user permissions for tus uploads

This commit is contained in:
Tobias Goerke 2023-07-26 15:58:24 +02:00
parent 9d04db7f96
commit 676dd2fec5
2 changed files with 31 additions and 20 deletions

View File

@ -72,6 +72,11 @@ func (th *TusHandler) getOrCreateTusdHandler(d *data, r *http.Request) (_ *tusd.
func (th TusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (th TusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
code, err := withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { code, err := withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
// Check if user has permission to create files
if !d.user.Perm.Create {
return http.StatusForbidden, nil
}
// Create a new tus handler for current user if it doesn't exist yet // Create a new tus handler for current user if it doesn't exist yet
tusdHandler, err := th.getOrCreateTusdHandler(d, r) tusdHandler, err := th.getOrCreateTusdHandler(d, r)
if err != nil { if err != nil {
@ -106,7 +111,7 @@ func (th TusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
func (th TusHandler) createTusdHandler(d *data, basePath string) (*tusd.UnroutedHandler, error) { func (th TusHandler) createTusdHandler(d *data, basePath string) (*tusd.UnroutedHandler, error) {
tusStore := NewInPlaceDataStore(d.user.FullPath("/")) tusStore := NewInPlaceDataStore(d.user.FullPath("/"), d.user.Perm.Modify)
composer := tusd.NewStoreComposer() composer := tusd.NewStoreComposer()
tusStore.UseIn(composer) tusStore.UseIn(composer)

View File

@ -30,6 +30,9 @@ type InPlaceDataStore struct {
// It equals the user's root directory. // It equals the user's root directory.
path string path string
// Store whether the user is permitted to modify files or only create new ones.
modifyPerm bool
// Maps an upload ID to its object. // Maps an upload ID to its object.
// Required, since GetUpload only provides us with the id of an upload // Required, since GetUpload only provides us with the id of an upload
// and expects us to return the Info object. // and expects us to return the Info object.
@ -41,11 +44,12 @@ type InPlaceDataStore struct {
mutex *sync.Mutex mutex *sync.Mutex
} }
func NewInPlaceDataStore(path string) *InPlaceDataStore { func NewInPlaceDataStore(path string, modifyPerm bool) *InPlaceDataStore {
return &InPlaceDataStore{ return &InPlaceDataStore{
path: path, path: path,
uploads: make(map[string]*InPlaceUpload), modifyPerm: modifyPerm,
mutex: &sync.Mutex{}, uploads: make(map[string]*InPlaceUpload),
mutex: &sync.Mutex{},
} }
} }
@ -54,7 +58,7 @@ func (store *InPlaceDataStore) UseIn(composer *tusd.StoreComposer) {
composer.UseConcater(store) composer.UseConcater(store)
} }
func (store *InPlaceDataStore) restoreState(filePath string, info *tusd.FileInfo) error { func (store *InPlaceDataStore) cleanupOrphanedUploads(filePath string) error {
// If the file doesn't exist, remove all upload references. // If the file doesn't exist, remove all upload references.
// This way we can eliminate inconsistencies for failed uploads. // This way we can eliminate inconsistencies for failed uploads.
if _, err := os.Stat(filePath); os.IsNotExist(err) { if _, err := os.Stat(filePath); os.IsNotExist(err) {
@ -75,6 +79,11 @@ func (store *InPlaceDataStore) restoreState(filePath string, info *tusd.FileInfo
} }
} }
if !uploadExists { if !uploadExists {
if !store.modifyPerm {
// Gets interpreted as a 400 by tusd.
// There is no way to return a 403, so a 400 is better than a 500.
return tusd.ErrUploadStoppedByServer
}
if err := os.Remove(filePath); err != nil { if err := os.Remove(filePath); err != nil {
return err return err
} }
@ -83,11 +92,11 @@ func (store *InPlaceDataStore) restoreState(filePath string, info *tusd.FileInfo
return nil return nil
} }
func (store *InPlaceDataStore) prepareFile(filePath string, info *tusd.FileInfo) (int64, error) { func (store *InPlaceDataStore) initializeUpload(filePath string, info *tusd.FileInfo) (int64, error) {
store.mutex.Lock() store.mutex.Lock()
defer store.mutex.Unlock() defer store.mutex.Unlock()
if err := store.restoreState(filePath, info); err != nil { if err := store.cleanupOrphanedUploads(filePath); err != nil {
return 0, err return 0, err
} }
@ -114,11 +123,8 @@ func (store *InPlaceDataStore) prepareFile(filePath string, info *tusd.FileInfo)
func (store *InPlaceDataStore) NewUpload(ctx context.Context, info tusd.FileInfo) (_ tusd.Upload, err error) { //nolint: gocritic func (store *InPlaceDataStore) NewUpload(ctx context.Context, info tusd.FileInfo) (_ tusd.Upload, err error) { //nolint: gocritic
// The method must return an unique id which is used to identify the upload // The method must return an unique id which is used to identify the upload
if info.ID == "" { if info.ID, err = uid(); err != nil {
info.ID, err = uid() return nil, err
if err != nil {
return nil, err
}
} }
destination, ok := info.MetaData["destination"] destination, ok := info.MetaData["destination"]
@ -136,7 +142,7 @@ func (store *InPlaceDataStore) NewUpload(ctx context.Context, info tusd.FileInfo
// Tus creates a POST request for the final concatenation. // Tus creates a POST request for the final concatenation.
// In that case, we don't need to create a new upload. // In that case, we don't need to create a new upload.
if !info.IsFinal { if !info.IsFinal {
if upload.actualOffset, err = store.prepareFile(filePath, &info); err != nil { if upload.actualOffset, err = store.initializeUpload(filePath, &info); err != nil {
return nil, err return nil, err
} }
store.uploads[info.ID] = upload store.uploads[info.ID] = upload
@ -207,18 +213,18 @@ func (upload *InPlaceUpload) FinishUpload(ctx context.Context) error {
} }
func (upload *InPlaceUpload) ConcatUploads(ctx context.Context, uploads []tusd.Upload) (err error) { func (upload *InPlaceUpload) ConcatUploads(ctx context.Context, uploads []tusd.Upload) (err error) {
for _, upload := range uploads { parent := upload.parent
delete((upload.(*InPlaceUpload)).parent.uploads, (upload.(*InPlaceUpload)).ID) for _, u := range uploads {
delete(parent.uploads, (u.(*InPlaceUpload)).ID)
} }
delete(upload.parent.uploads, upload.ID) delete(parent.uploads, upload.ID)
return nil return nil
} }
func uid() (string, error) { func uid() (string, error) {
id := make([]byte, uidLength) id := make([]byte, uidLength)
_, err := io.ReadFull(rand.Reader, id) if _, err := io.ReadFull(rand.Reader, id); err != nil {
if err != nil {
return "", err return "", err
} }
return hex.EncodeToString(id), err return hex.EncodeToString(id), nil
} }