add per directory permissions

we can now have permissions such as these ones

{"/":["*"],"/somedir":["list","download"]}

The old permissions are automatically converted to the new structure,
no database migration is needed
This commit is contained in:
Nicola Murino 2019-12-25 18:20:19 +01:00
parent f8fd5c067c
commit 489101668c
20 changed files with 1166 additions and 273 deletions

View file

@ -12,7 +12,7 @@ Full featured and highly configurable SFTP server
- Quota support: accounts can have individual quota expressed as max total size and/or max number of files. - Quota support: accounts can have individual quota expressed as max total size and/or max number of files.
- Bandwidth throttling is supported, with distinct settings for upload and download. - Bandwidth throttling is supported, with distinct settings for upload and download.
- Per user maximum concurrent sessions. - Per user maximum concurrent sessions.
- Per user permissions: list directories content, upload, overwrite, download, delete, rename, create directories, create symlinks, changing owner/group and mode, changing access and modification times can be enabled or disabled. - Per user and per directory permissions: list directories content, upload, overwrite, download, delete, rename, create directories, create symlinks, changing owner/group and mode, changing access and modification times can be enabled or disabled.
- Per user files/folders ownership: you can map all the users to the system account that runs SFTPGo (all platforms are supported) or you can run SFTPGo as root user and map each user or group of users to a different system account (*NIX only). - Per user files/folders ownership: you can map all the users to the system account that runs SFTPGo (all platforms are supported) or you can run SFTPGo as root user and map each user or group of users to a different system account (*NIX only).
- Configurable custom commands and/or HTTP notifications on file upload, download, delete, rename, on SSH commands and on user add, update and delete. - Configurable custom commands and/or HTTP notifications on file upload, download, delete, rename, on SSH commands and on user add, update and delete.
- Automatically terminating idle connections. - Automatically terminating idle connections.
@ -367,7 +367,7 @@ For each account the following properties can be configured:
- `max_sessions` maximum concurrent sessions. 0 means unlimited. - `max_sessions` maximum concurrent sessions. 0 means unlimited.
- `quota_size` maximum size allowed as bytes. 0 means unlimited. - `quota_size` maximum size allowed as bytes. 0 means unlimited.
- `quota_files` maximum number of files allowed. 0 means unlimited. - `quota_files` maximum number of files allowed. 0 means unlimited.
- `permissions` the following permissions are supported: - `permissions` the following per directory permissions are supported:
- `*` all permissions are granted - `*` all permissions are granted
- `list` list items is allowed - `list` list items is allowed
- `download` download files is allowed - `download` download files is allowed

View file

@ -33,6 +33,8 @@ Please take a look at the usage below to customize the serving parameters`,
if !filepath.IsAbs(portableDir) { if !filepath.IsAbs(portableDir) {
portableDir, _ = filepath.Abs(portableDir) portableDir, _ = filepath.Abs(portableDir)
} }
permissions := make(map[string][]string)
permissions["/"] = portablePermissions
service := service.Service{ service := service.Service{
ConfigDir: defaultConfigDir, ConfigDir: defaultConfigDir,
ConfigFile: defaultConfigName, ConfigFile: defaultConfigName,
@ -48,7 +50,7 @@ Please take a look at the usage below to customize the serving parameters`,
Username: portableUsername, Username: portableUsername,
Password: portablePassword, Password: portablePassword,
PublicKeys: portablePublicKeys, PublicKeys: portablePublicKeys,
Permissions: portablePermissions, Permissions: permissions,
HomeDir: portableDir, HomeDir: portableDir,
Status: 1, Status: 1,
}, },

View file

@ -14,7 +14,7 @@ import (
) )
const ( const (
databaseVersion = 2 databaseVersion = 3
) )
var ( var (
@ -33,6 +33,28 @@ type boltDatabaseVersion struct {
Version int Version int
} }
type compatUserV2 struct {
ID int64 `json:"id"`
Username string `json:"username"`
Password string `json:"password,omitempty"`
PublicKeys []string `json:"public_keys,omitempty"`
HomeDir string `json:"home_dir"`
UID int `json:"uid"`
GID int `json:"gid"`
MaxSessions int `json:"max_sessions"`
QuotaSize int64 `json:"quota_size"`
QuotaFiles int `json:"quota_files"`
Permissions []string `json:"permissions"`
UsedQuotaSize int64 `json:"used_quota_size"`
UsedQuotaFiles int `json:"used_quota_files"`
LastQuotaUpdate int64 `json:"last_quota_update"`
UploadBandwidth int64 `json:"upload_bandwidth"`
DownloadBandwidth int64 `json:"download_bandwidth"`
ExpirationDate int64 `json:"expiration_date"`
LastLogin int64 `json:"last_login"`
Status int `json:"status"`
}
func initializeBoltProvider(basePath string) error { func initializeBoltProvider(basePath string) error {
var err error var err error
logSender = BoltDataProviderName logSender = BoltDataProviderName
@ -376,7 +398,20 @@ func checkBoltDatabaseVersion(dbHandle *bolt.DB) error {
return nil return nil
} }
if dbVersion.Version == 1 { if dbVersion.Version == 1 {
providerLog(logger.LevelInfo, "update bolt database version: 1 -> 2") err = updateDatabaseFrom1To2(dbHandle)
if err != nil {
return err
}
return updateDatabaseFrom2To3(dbHandle)
} else if dbVersion.Version == 2 {
return updateDatabaseFrom2To3(dbHandle)
}
return nil
}
func updateDatabaseFrom1To2(dbHandle *bolt.DB) error {
providerLog(logger.LevelInfo, "updating bolt database version: 1 -> 2")
usernames, err := getBoltAvailableUsernames(dbHandle) usernames, err := getBoltAvailableUsernames(dbHandle)
if err != nil { if err != nil {
return err return err
@ -396,8 +431,59 @@ func checkBoltDatabaseVersion(dbHandle *bolt.DB) error {
return updateBoltDatabaseVersion(dbHandle, 2) return updateBoltDatabaseVersion(dbHandle, 2)
} }
func updateDatabaseFrom2To3(dbHandle *bolt.DB) error {
providerLog(logger.LevelInfo, "updating bolt database version: 2 -> 3")
users := []User{}
err := dbHandle.View(func(tx *bolt.Tx) error {
bucket, _, err := getBuckets(tx)
if err != nil {
return err return err
} }
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var compatUser compatUserV2
err = json.Unmarshal(v, &compatUser)
if err == nil {
user := User{}
user.ID = compatUser.ID
user.Username = compatUser.Username
user.Password = compatUser.Password
user.PublicKeys = compatUser.PublicKeys
user.HomeDir = compatUser.HomeDir
user.UID = compatUser.UID
user.GID = compatUser.GID
user.MaxSessions = compatUser.MaxSessions
user.QuotaSize = compatUser.QuotaSize
user.QuotaFiles = compatUser.QuotaFiles
user.Permissions = make(map[string][]string)
user.Permissions["/"] = compatUser.Permissions
user.UsedQuotaSize = compatUser.UsedQuotaSize
user.UsedQuotaFiles = compatUser.UsedQuotaFiles
user.LastQuotaUpdate = compatUser.LastQuotaUpdate
user.UploadBandwidth = compatUser.UploadBandwidth
user.DownloadBandwidth = compatUser.DownloadBandwidth
user.ExpirationDate = compatUser.ExpirationDate
user.LastLogin = compatUser.LastLogin
user.Status = compatUser.Status
users = append(users, user)
}
}
return err
})
if err != nil {
return err
}
for _, user := range users {
err = provider.updateUser(user)
if err != nil {
return err
}
providerLog(logger.LevelInfo, "user %#v updated, \"permissions\" setted to %+v", user.Username, user.Permissions)
}
return updateBoltDatabaseVersion(dbHandle, 3)
}
func getBoltAvailableUsernames(dbHandle *bolt.DB) ([]string, error) { func getBoltAvailableUsernames(dbHandle *bolt.DB) ([]string, error) {
usernames := []string{} usernames := []string{}

View file

@ -18,6 +18,7 @@ import (
"net/url" "net/url"
"os" "os"
"os/exec" "os/exec"
"path"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
@ -343,14 +344,33 @@ func buildUserHomeDir(user *User) {
} }
func validatePermissions(user *User) error { func validatePermissions(user *User) error {
for _, p := range user.Permissions { permissions := make(map[string][]string)
if _, ok := user.Permissions["/"]; !ok {
return &ValidationError{err: fmt.Sprintf("Permissions for the root dir \"/\" must be set")}
}
for dir, perms := range user.Permissions {
if len(perms) == 0 {
return &ValidationError{err: fmt.Sprintf("No permissions granted for the directory: %#v", dir)}
}
for _, p := range perms {
if !utils.IsStringInSlice(p, ValidPerms) { if !utils.IsStringInSlice(p, ValidPerms) {
return &ValidationError{err: fmt.Sprintf("Invalid permission: %v", p)} return &ValidationError{err: fmt.Sprintf("Invalid permission: %#v", p)}
} }
} }
if utils.IsStringInSlice(PermAny, user.Permissions) { cleanedDir := filepath.ToSlash(path.Clean(dir))
user.Permissions = []string{PermAny} if cleanedDir != "/" {
cleanedDir = strings.TrimSuffix(cleanedDir, "/")
} }
if !path.IsAbs(cleanedDir) {
return &ValidationError{err: fmt.Sprintf("Cannot set permissions for non absolute path: %#v", dir)}
}
if utils.IsStringInSlice(PermAny, perms) {
permissions[cleanedDir] = []string{PermAny}
} else {
permissions[cleanedDir] = perms
}
}
user.Permissions = permissions
return nil return nil
} }

View file

@ -265,10 +265,18 @@ func getUserFromDbRow(row *sql.Row, rows *sql.Rows) (User, error) {
} }
} }
if permissions.Valid { if permissions.Valid {
perms := make(map[string][]string)
err = json.Unmarshal([]byte(permissions.String), &perms)
if err == nil {
user.Permissions = perms
} else {
// compatibility layer: until version 0.9.4 permissions were a string list
var list []string var list []string
err = json.Unmarshal([]byte(permissions.String), &list) err = json.Unmarshal([]byte(permissions.String), &list)
if err == nil { if err == nil {
user.Permissions = list perms["/"] = list
user.Permissions = perms
}
} }
} }
return user, err return user, err

View file

@ -3,8 +3,10 @@ package dataprovider
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"path"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings"
"github.com/drakkan/sftpgo/utils" "github.com/drakkan/sftpgo/utils"
) )
@ -68,7 +70,7 @@ type User struct {
// Maximum number of files allowed. 0 means unlimited // Maximum number of files allowed. 0 means unlimited
QuotaFiles int `json:"quota_files"` QuotaFiles int `json:"quota_files"`
// List of the granted permissions // List of the granted permissions
Permissions []string `json:"permissions"` Permissions map[string][]string `json:"permissions"`
// Used quota as bytes // Used quota as bytes
UsedQuotaSize int64 `json:"used_quota_size"` UsedQuotaSize int64 `json:"used_quota_size"`
// Used quota as number of files // Used quota as number of files
@ -83,21 +85,59 @@ type User struct {
LastLogin int64 `json:"last_login"` LastLogin int64 `json:"last_login"`
} }
// GetPermissionsForPath returns the permissions for the given path
func (u *User) GetPermissionsForPath(p string) []string {
permissions := []string{}
if perms, ok := u.Permissions["/"]; ok {
// if only root permissions are defined returns them unconditionally
if len(u.Permissions) == 1 {
return perms
}
// fallback permissions
permissions = perms
}
relPath := u.GetRelativePath(p)
if len(relPath) == 0 {
relPath = "/"
}
dirsForPath := []string{relPath}
for {
if relPath == "/" {
break
}
relPath = path.Dir(relPath)
dirsForPath = append(dirsForPath, relPath)
}
// dirsForPath contains all the dirs for a given path in reverse order
// for example if the path is: /1/2/3/4 it contains:
// [ "/1/2/3/4", "/1/2/3", "/1/2", "/1", "/" ]
// so the first match is the one we are interested to
for _, val := range dirsForPath {
if perms, ok := u.Permissions[val]; ok {
permissions = perms
break
}
}
return permissions
}
// HasPerm returns true if the user has the given permission or any permission // HasPerm returns true if the user has the given permission or any permission
func (u *User) HasPerm(permission string) bool { func (u *User) HasPerm(permission, path string) bool {
if utils.IsStringInSlice(PermAny, u.Permissions) { perms := u.GetPermissionsForPath(path)
if utils.IsStringInSlice(PermAny, perms) {
return true return true
} }
return utils.IsStringInSlice(permission, u.Permissions) return utils.IsStringInSlice(permission, perms)
} }
// HasPerms return true if the user has all the given permissions // HasPerms return true if the user has all the given permissions
func (u *User) HasPerms(permissions []string) bool { func (u *User) HasPerms(permissions []string, path string) bool {
if utils.IsStringInSlice(PermAny, u.Permissions) { perms := u.GetPermissionsForPath(path)
if utils.IsStringInSlice(PermAny, perms) {
return true return true
} }
for _, permission := range permissions { for _, permission := range permissions {
if !utils.IsStringInSlice(permission, u.Permissions) { if !utils.IsStringInSlice(permission, perms) {
return false return false
} }
} }
@ -143,10 +183,13 @@ func (u *User) HasQuotaRestrictions() bool {
// GetRelativePath returns the path for a file relative to the user's home dir. // GetRelativePath returns the path for a file relative to the user's home dir.
// This is the path as seen by SFTP users // This is the path as seen by SFTP users
func (u *User) GetRelativePath(path string) string { func (u *User) GetRelativePath(path string) string {
rel, err := filepath.Rel(u.GetHomeDir(), path) rel, err := filepath.Rel(u.GetHomeDir(), filepath.Clean(path))
if err != nil { if err != nil {
return "" return ""
} }
if rel == "." || strings.HasPrefix(rel, "..") {
rel = ""
}
return "/" + filepath.ToSlash(rel) return "/" + filepath.ToSlash(rel)
} }
@ -168,12 +211,28 @@ func (u *User) GetQuotaSummary() string {
// GetPermissionsAsString returns the user's permissions as comma separated string // GetPermissionsAsString returns the user's permissions as comma separated string
func (u *User) GetPermissionsAsString() string { func (u *User) GetPermissionsAsString() string {
var result string result := ""
for _, p := range u.Permissions { for dir, perms := range u.Permissions {
var dirPerms string
for _, p := range perms {
if len(dirPerms) > 0 {
dirPerms += ", "
}
dirPerms += p
}
dp := fmt.Sprintf("%#v: %#v", dir, dirPerms)
if dir == "/" {
if len(result) > 0 {
result = dp + ", " + result
} else {
result = dp
}
} else {
if len(result) > 0 { if len(result) > 0 {
result += ", " result += ", "
} }
result += p result += dp
}
} }
return result return result
} }
@ -230,8 +289,12 @@ func (u *User) GetExpirationDateAsString() string {
func (u *User) getACopy() User { func (u *User) getACopy() User {
pubKeys := make([]string, len(u.PublicKeys)) pubKeys := make([]string, len(u.PublicKeys))
copy(pubKeys, u.PublicKeys) copy(pubKeys, u.PublicKeys)
permissions := make([]string, len(u.Permissions)) permissions := make(map[string][]string)
copy(permissions, u.Permissions) for k, v := range u.Permissions {
perms := make([]string, len(v))
copy(perms, v)
permissions[k] = perms
}
return User{ return User{
ID: u.ID, ID: u.ID,
Username: u.Username, Username: u.Username,

View file

@ -102,6 +102,8 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
return return
} }
user, err := dataprovider.GetUserByID(dataProvider, userID) user, err := dataprovider.GetUserByID(dataProvider, userID)
oldPermissions := user.Permissions
user.Permissions = make(map[string][]string)
if _, ok := err.(*dataprovider.RecordNotFoundError); ok { if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
sendAPIResponse(w, r, err, "", http.StatusNotFound) sendAPIResponse(w, r, err, "", http.StatusNotFound)
return return
@ -114,6 +116,10 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, err, "", http.StatusBadRequest) sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return return
} }
// we use new Permissions if passed otherwise the old ones
if len(user.Permissions) == 0 {
user.Permissions = oldPermissions
}
if user.ID != userID { if user.ID != userID {
sendAPIResponse(w, r, err, "user ID in request body does not match user ID in path parameter", http.StatusBadRequest) sendAPIResponse(w, r, err, "user ID in request body does not match user ID in path parameter", http.StatusBadRequest)
return return

View file

@ -329,11 +329,17 @@ func checkUser(expected dataprovider.User, actual dataprovider.User) error {
return errors.New("user ID mismatch") return errors.New("user ID mismatch")
} }
} }
for _, v := range expected.Permissions { for dir, perms := range expected.Permissions {
if !utils.IsStringInSlice(v, actual.Permissions) { if actualPerms, ok := actual.Permissions[dir]; ok {
for _, v := range actualPerms {
if !utils.IsStringInSlice(v, perms) {
return errors.New("Permissions contents mismatch") return errors.New("Permissions contents mismatch")
} }
} }
} else {
return errors.New("Permissions directories mismatch")
}
}
return compareEqualsUserFields(expected, actual) return compareEqualsUserFields(expected, actual)
} }

View file

@ -194,20 +194,31 @@ func TestAddUserInvalidHomeDir(t *testing.T) {
func TestAddUserNoPerms(t *testing.T) { func TestAddUserNoPerms(t *testing.T) {
u := getTestUser() u := getTestUser()
u.Permissions = []string{} u.Permissions = make(map[string][]string)
_, _, err := httpd.AddUser(u, http.StatusBadRequest) _, _, err := httpd.AddUser(u, http.StatusBadRequest)
if err != nil { if err != nil {
t.Errorf("unexpected error adding user with no perms: %v", err) t.Errorf("unexpected error adding user with no perms: %v", err)
} }
u.Permissions["/"] = []string{}
_, _, err = httpd.AddUser(u, http.StatusBadRequest)
if err != nil {
t.Errorf("unexpected error adding user with no perms: %v", err)
}
} }
func TestAddUserInvalidPerms(t *testing.T) { func TestAddUserInvalidPerms(t *testing.T) {
u := getTestUser() u := getTestUser()
u.Permissions = []string{"invalidPerm"} u.Permissions["/"] = []string{"invalidPerm"}
_, _, err := httpd.AddUser(u, http.StatusBadRequest) _, _, err := httpd.AddUser(u, http.StatusBadRequest)
if err != nil { if err != nil {
t.Errorf("unexpected error adding user with no perms: %v", err) t.Errorf("unexpected error adding user with no perms: %v", err)
} }
// permissions for root dir are mandatory
u.Permissions["/somedir"] = []string{dataprovider.PermAny}
_, _, err = httpd.AddUser(u, http.StatusBadRequest)
if err != nil {
t.Errorf("unexpected error adding user with no perms: %v", err)
}
} }
func TestUserPublicKey(t *testing.T) { func TestUserPublicKey(t *testing.T) {
@ -251,7 +262,8 @@ func TestUpdateUser(t *testing.T) {
user.MaxSessions = 10 user.MaxSessions = 10
user.QuotaSize = 4096 user.QuotaSize = 4096
user.QuotaFiles = 2 user.QuotaFiles = 2
user.Permissions = []string{dataprovider.PermCreateDirs, dataprovider.PermDelete, dataprovider.PermDownload} user.Permissions["/"] = []string{dataprovider.PermCreateDirs, dataprovider.PermDelete, dataprovider.PermDownload}
user.Permissions["/subdir"] = []string{dataprovider.PermListItems, dataprovider.PermUpload}
user.UploadBandwidth = 1024 user.UploadBandwidth = 1024
user.DownloadBandwidth = 512 user.DownloadBandwidth = 512
user, _, err = httpd.UpdateUser(user, http.StatusOK) user, _, err = httpd.UpdateUser(user, http.StatusOK)
@ -556,7 +568,7 @@ func TestBasicUserHandlingMock(t *testing.T) {
checkResponseCode(t, http.StatusInternalServerError, rr.Code) checkResponseCode(t, http.StatusInternalServerError, rr.Code)
user.MaxSessions = 10 user.MaxSessions = 10
user.UploadBandwidth = 128 user.UploadBandwidth = 128
user.Permissions = []string{dataprovider.PermAny, dataprovider.PermDelete, dataprovider.PermDownload} user.Permissions["/"] = []string{dataprovider.PermAny, dataprovider.PermDelete, dataprovider.PermDownload}
userAsJSON = getUserAsJSON(t, user) userAsJSON = getUserAsJSON(t, user)
req, _ = http.NewRequest(http.MethodPut, userPath+"/"+strconv.FormatInt(user.ID, 10), bytes.NewBuffer(userAsJSON)) req, _ = http.NewRequest(http.MethodPut, userPath+"/"+strconv.FormatInt(user.ID, 10), bytes.NewBuffer(userAsJSON))
rr = executeRequest(req) rr = executeRequest(req)
@ -574,10 +586,10 @@ func TestBasicUserHandlingMock(t *testing.T) {
if user.MaxSessions != updatedUser.MaxSessions || user.UploadBandwidth != updatedUser.UploadBandwidth { if user.MaxSessions != updatedUser.MaxSessions || user.UploadBandwidth != updatedUser.UploadBandwidth {
t.Errorf("Error modifying user actual: %v, %v", updatedUser.MaxSessions, updatedUser.UploadBandwidth) t.Errorf("Error modifying user actual: %v, %v", updatedUser.MaxSessions, updatedUser.UploadBandwidth)
} }
if len(updatedUser.Permissions) != 1 { if len(updatedUser.Permissions["/"]) != 1 {
t.Errorf("permissions other than any should be removed") t.Errorf("permissions other than any should be removed")
} }
if !utils.IsStringInSlice(dataprovider.PermAny, updatedUser.Permissions) { if !utils.IsStringInSlice(dataprovider.PermAny, updatedUser.Permissions["/"]) {
t.Errorf("permissions mismatch") t.Errorf("permissions mismatch")
} }
req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil) req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil)
@ -614,7 +626,7 @@ func TestAddUserInvalidHomeDirMock(t *testing.T) {
func TestAddUserInvalidPermsMock(t *testing.T) { func TestAddUserInvalidPermsMock(t *testing.T) {
user := getTestUser() user := getTestUser()
user.Permissions = []string{} user.Permissions["/"] = []string{}
userAsJSON := getUserAsJSON(t, user) userAsJSON := getUserAsJSON(t, user)
req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON)) req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))
rr := executeRequest(req) rr := executeRequest(req)
@ -627,6 +639,112 @@ func TestAddUserInvalidJsonMock(t *testing.T) {
checkResponseCode(t, http.StatusBadRequest, rr.Code) checkResponseCode(t, http.StatusBadRequest, rr.Code)
} }
func TestUpdateUserMock(t *testing.T) {
user := getTestUser()
userAsJSON := getUserAsJSON(t, user)
req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))
rr := executeRequest(req)
checkResponseCode(t, http.StatusOK, rr.Code)
err := render.DecodeJSON(rr.Body, &user)
if err != nil {
t.Errorf("Error get user: %v", err)
}
// permissions should not change if empty or nil
permissions := user.Permissions
user.Permissions = make(map[string][]string)
userAsJSON = getUserAsJSON(t, user)
req, _ = http.NewRequest(http.MethodPut, userPath+"/"+strconv.FormatInt(user.ID, 10), bytes.NewBuffer(userAsJSON))
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr.Code)
req, _ = http.NewRequest(http.MethodGet, userPath+"/"+strconv.FormatInt(user.ID, 10), nil)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr.Code)
var updatedUser dataprovider.User
err = render.DecodeJSON(rr.Body, &updatedUser)
if err != nil {
t.Errorf("Error decoding updated user: %v", err)
}
for dir, perms := range permissions {
if actualPerms, ok := updatedUser.Permissions[dir]; ok {
for _, v := range actualPerms {
if !utils.IsStringInSlice(v, perms) {
t.Error("Permissions contents mismatch")
}
}
} else {
t.Error("Permissions directories mismatch")
}
}
req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr.Code)
}
func TestUserPermissionsMock(t *testing.T) {
user := getTestUser()
user.Permissions = make(map[string][]string)
user.Permissions["/somedir"] = []string{dataprovider.PermAny}
userAsJSON := getUserAsJSON(t, user)
req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))
rr := executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr.Code)
user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermAny}
user.Permissions[".."] = []string{dataprovider.PermAny}
userAsJSON = getUserAsJSON(t, user)
req, _ = http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))
rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr.Code)
user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermAny}
userAsJSON = getUserAsJSON(t, user)
req, _ = http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr.Code)
err := render.DecodeJSON(rr.Body, &user)
if err != nil {
t.Errorf("Error get user: %v", err)
}
user.Permissions["/somedir"] = []string{}
userAsJSON = getUserAsJSON(t, user)
req, _ = http.NewRequest(http.MethodPut, userPath+"/"+strconv.FormatInt(user.ID, 10), bytes.NewBuffer(userAsJSON))
rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr.Code)
delete(user.Permissions, "/somedir")
user.Permissions["not_abs_path"] = []string{dataprovider.PermAny}
userAsJSON = getUserAsJSON(t, user)
req, _ = http.NewRequest(http.MethodPut, userPath+"/"+strconv.FormatInt(user.ID, 10), bytes.NewBuffer(userAsJSON))
rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr.Code)
delete(user.Permissions, "not_abs_path")
user.Permissions["/somedir/../otherdir/"] = []string{dataprovider.PermListItems}
userAsJSON = getUserAsJSON(t, user)
req, _ = http.NewRequest(http.MethodPut, userPath+"/"+strconv.FormatInt(user.ID, 10), bytes.NewBuffer(userAsJSON))
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr.Code)
req, _ = http.NewRequest(http.MethodGet, userPath+"/"+strconv.FormatInt(user.ID, 10), nil)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr.Code)
var updatedUser dataprovider.User
err = render.DecodeJSON(rr.Body, &updatedUser)
if err != nil {
t.Errorf("Error decoding updated user: %v", err)
}
if val, ok := updatedUser.Permissions["/otherdir"]; ok {
if !utils.IsStringInSlice(dataprovider.PermListItems, val) {
t.Error("expected permission list not found")
}
if len(val) != 1 {
t.Errorf("Unexpected number of permissions, expected 1, actual: %v", len(val))
}
} else {
t.Errorf("expected dir not found in permissions")
}
req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr.Code)
}
func TestUpdateUserInvalidJsonMock(t *testing.T) { func TestUpdateUserInvalidJsonMock(t *testing.T) {
user := getTestUser() user := getTestUser()
userAsJSON := getUserAsJSON(t, user) userAsJSON := getUserAsJSON(t, user)
@ -924,6 +1042,7 @@ func TestWebUserAddMock(t *testing.T) {
form.Set("status", strconv.Itoa(user.Status)) form.Set("status", strconv.Itoa(user.Status))
form.Set("expiration_date", "") form.Set("expiration_date", "")
form.Set("permissions", "*") form.Set("permissions", "*")
form.Set("sub_dirs_permissions", "/subdir:list,download")
// test invalid url escape // test invalid url escape
req, _ := http.NewRequest(http.MethodPost, webUserPath+"?a=%2", strings.NewReader(form.Encode())) req, _ := http.NewRequest(http.MethodPost, webUserPath+"?a=%2", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
@ -1050,6 +1169,7 @@ func TestWebUserUpdateMock(t *testing.T) {
form.Set("upload_bandwidth", "0") form.Set("upload_bandwidth", "0")
form.Set("download_bandwidth", "0") form.Set("download_bandwidth", "0")
form.Set("permissions", "*") form.Set("permissions", "*")
form.Set("sub_dirs_permissions", "/otherdir:list,upload")
form.Set("status", strconv.Itoa(user.Status)) form.Set("status", strconv.Itoa(user.Status))
form.Set("expiration_date", "2020-01-01 00:00:00") form.Set("expiration_date", "2020-01-01 00:00:00")
req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), strings.NewReader(form.Encode())) req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), strings.NewReader(form.Encode()))
@ -1142,13 +1262,15 @@ func waitTCPListening(address string) {
} }
func getTestUser() dataprovider.User { func getTestUser() dataprovider.User {
return dataprovider.User{ user := dataprovider.User{
Username: defaultUsername, Username: defaultUsername,
Password: defaultPassword, Password: defaultPassword,
HomeDir: filepath.Join(homeBasePath, defaultUsername), HomeDir: filepath.Join(homeBasePath, defaultUsername),
Permissions: defaultPerms,
Status: 1, Status: 1,
} }
user.Permissions = make(map[string][]string)
user.Permissions["/"] = defaultPerms
return user
} }
func getUserAsJSON(t *testing.T, user dataprovider.User) []byte { func getUserAsJSON(t *testing.T, user dataprovider.User) []byte {

View file

@ -69,13 +69,23 @@ func TestCheckUser(t *testing.T) {
} }
expected.ID = 2 expected.ID = 2
actual.ID = 2 actual.ID = 2
expected.Permissions = []string{dataprovider.PermCreateDirs, dataprovider.PermDelete, dataprovider.PermDownload} expected.Permissions = make(map[string][]string)
actual.Permissions = []string{dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks} expected.Permissions["/"] = []string{dataprovider.PermCreateDirs, dataprovider.PermDelete, dataprovider.PermDownload}
actual.Permissions = make(map[string][]string)
actual.Permissions["/"] = []string{dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks}
err = checkUser(expected, actual) err = checkUser(expected, actual)
if err == nil { if err == nil {
t.Errorf("Permissions are not equal") t.Errorf("Permissions are not equal")
} }
expected.Permissions = append(expected.Permissions, dataprovider.PermRename) expected.Permissions["/"] = append(expected.Permissions["/"], dataprovider.PermRename)
err = checkUser(expected, actual)
if err == nil {
t.Errorf("Permissions are not equal")
}
expected.Permissions = make(map[string][]string)
expected.Permissions["/somedir"] = []string{dataprovider.PermAny}
actual.Permissions = make(map[string][]string)
actual.Permissions["/otherdir"] = []string{dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks}
err = checkUser(expected, actual) err = checkUser(expected, actual)
if err == nil { if err == nil {
t.Errorf("Permissions are not equal") t.Errorf("Permissions are not equal")
@ -85,6 +95,8 @@ func TestCheckUser(t *testing.T) {
func TestCompareUserFields(t *testing.T) { func TestCompareUserFields(t *testing.T) {
expected := dataprovider.User{} expected := dataprovider.User{}
actual := dataprovider.User{} actual := dataprovider.User{}
expected.Permissions = make(map[string][]string)
actual.Permissions = make(map[string][]string)
expected.Username = "test" expected.Username = "test"
err := compareEqualsUserFields(expected, actual) err := compareEqualsUserFields(expected, actual)
if err == nil { if err == nil {
@ -127,7 +139,7 @@ func TestCompareUserFields(t *testing.T) {
t.Errorf("QuotaFiles do not match") t.Errorf("QuotaFiles do not match")
} }
expected.QuotaFiles = 0 expected.QuotaFiles = 0
expected.Permissions = []string{dataprovider.PermCreateDirs} expected.Permissions["/"] = []string{dataprovider.PermCreateDirs}
err = compareEqualsUserFields(expected, actual) err = compareEqualsUserFields(expected, actual)
if err == nil { if err == nil {
t.Errorf("Permissions are not equal") t.Errorf("Permissions are not equal")

View file

@ -2,7 +2,7 @@ openapi: 3.0.1
info: info:
title: SFTPGo title: SFTPGo
description: 'SFTPGo REST API' description: 'SFTPGo REST API'
version: 1.2.0 version: 1.3.0
servers: servers:
- url: /api/v1 - url: /api/v1
@ -560,6 +560,15 @@ components:
* `chmod` changing file or directory permissions is allowed * `chmod` changing file or directory permissions is allowed
* `chown` changing file or directory owner and group is allowed * `chown` changing file or directory owner and group is allowed
* `chtimes` changing file or directory access and modification time is allowed * `chtimes` changing file or directory access and modification time is allowed
DirPermissions:
type: object
additionalProperties:
type: array
items:
$ref: '#/components/schemas/Permission'
minItems: 1
minProperties: 1
description: hash map with directory as key and an array of permissions as value. Directories must be absolute paths, permissions for root directory ("/") are required
User: User:
type: object type: object
properties: properties:
@ -620,10 +629,11 @@ components:
format: int32 format: int32
description: quota as number of files. 0 menas unlimited. Please note that quota is updated if files are added/removed via SFTP/SCP otherwise a quota scan is needed description: quota as number of files. 0 menas unlimited. Please note that quota is updated if files are added/removed via SFTP/SCP otherwise a quota scan is needed
permissions: permissions:
type: array type: object
items: items:
$ref: '#/components/schemas/Permission' $ref: '#/components/schemas/DirPermissions'
minItems: 1 minItems: 1
example: {"/":["*"],"/somedir":["list","download"]}
used_quota_size: used_quota_size:
type: integer type: integer
format: int64 format: int64

View file

@ -63,8 +63,10 @@ type userPage struct {
basePage basePage
IsAdd bool IsAdd bool
User dataprovider.User User dataprovider.User
RootPerms []string
Error string Error string
ValidPerms []string ValidPerms []string
RootDirPerms []string
} }
type messagePage struct { type messagePage struct {
@ -161,6 +163,7 @@ func renderAddUserPage(w http.ResponseWriter, user dataprovider.User, error stri
Error: error, Error: error,
User: user, User: user,
ValidPerms: dataprovider.ValidPerms, ValidPerms: dataprovider.ValidPerms,
RootDirPerms: user.GetPermissionsForPath("/"),
} }
renderTemplate(w, templateUser, data) renderTemplate(w, templateUser, data)
} }
@ -172,10 +175,37 @@ func renderUpdateUserPage(w http.ResponseWriter, user dataprovider.User, error s
Error: error, Error: error,
User: user, User: user,
ValidPerms: dataprovider.ValidPerms, ValidPerms: dataprovider.ValidPerms,
RootDirPerms: user.GetPermissionsForPath("/"),
} }
renderTemplate(w, templateUser, data) renderTemplate(w, templateUser, data)
} }
func getUserPermissionsFromPostFields(r *http.Request) map[string][]string {
permissions := make(map[string][]string)
permissions["/"] = r.Form["permissions"]
subDirsPermsValue := r.Form.Get("sub_dirs_permissions")
for _, v := range strings.Split(subDirsPermsValue, "\n") {
cleaned := strings.TrimSpace(v)
if len(cleaned) > 0 && strings.ContainsRune(cleaned, ':') {
dirPerms := strings.Split(cleaned, ":")
if len(dirPerms) > 1 {
dir := dirPerms[0]
perms := []string{}
for _, p := range strings.Split(dirPerms[1], ",") {
cleanedPerm := strings.TrimSpace(p)
if len(cleanedPerm) > 0 {
perms = append(perms, cleanedPerm)
}
}
if len(dir) > 0 && len(perms) > 0 {
permissions[dir] = perms
}
}
}
}
return permissions
}
func getUserFromPostFields(r *http.Request) (dataprovider.User, error) { func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
var user dataprovider.User var user dataprovider.User
err := r.ParseForm() err := r.ParseForm()
@ -238,7 +268,7 @@ func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
HomeDir: r.Form.Get("home_dir"), HomeDir: r.Form.Get("home_dir"),
UID: uid, UID: uid,
GID: gid, GID: gid,
Permissions: r.Form["permissions"], Permissions: getUserPermissionsFromPostFields(r),
MaxSessions: maxSessions, MaxSessions: maxSessions,
QuotaSize: quotaSize, QuotaSize: quotaSize,
QuotaFiles: quotaFiles, QuotaFiles: quotaFiles,

View file

@ -41,22 +41,23 @@ Let's see a sample usage for each REST API.
Command: Command:
``` ```
python sftpgo_api_cli.py add-user test_username --password "test_pwd" --home-dir="/tmp/test_home_dir" --uid 33 --gid 1000 --max-sessions 2 --quota-size 0 --quota-files 3 --permissions "list" "download" "upload" "delete" "rename" "create_dirs" "overwrite" --upload-bandwidth 100 --download-bandwidth 60 --status 0 --expiration-date 2019-01-01 python sftpgo_api_cli.py add-user test_username --password "test_pwd" --home-dir="/tmp/test_home_dir" --uid 33 --gid 1000 --max-sessions 2 --quota-size 0 --quota-files 3 --permissions "list" "download" "upload" "delete" "rename" "create_dirs" "overwrite" --subdirs-permissions "/dir1:list,download" "/dir2:*" --upload-bandwidth 100 --download-bandwidth 60 --status 0 --expiration-date 2019-01-01
``` ```
Output: Output:
```json ```json
{ {
"id": 5140, "download_bandwidth": 60,
"username": "test_username", "expiration_date": 1546297200000,
"home_dir": "/tmp/test_home_dir",
"uid": 33,
"gid": 1000, "gid": 1000,
"home_dir": "/tmp/test_home_dir",
"id": 9576,
"last_login": 0,
"last_quota_update": 0,
"max_sessions": 2, "max_sessions": 2,
"quota_size": 0, "permissions": {
"quota_files": 3, "/": [
"permissions": [
"list", "list",
"download", "download",
"upload", "upload",
@ -65,14 +66,22 @@ Output:
"create_dirs", "create_dirs",
"overwrite" "overwrite"
], ],
"used_quota_size": 0, "/dir1": [
"used_quota_files": 0, "list",
"last_quota_update": 0, "download"
"last_login": 0, ],
"expiration_date": 1546297200000, "/dir2": [
"*"
]
},
"quota_files": 3,
"quota_size": 0,
"status": 0, "status": 0,
"uid": 33,
"upload_bandwidth": 100, "upload_bandwidth": 100,
"download_bandwidth": 60 "used_quota_files": 0,
"used_quota_size": 0,
"username": "test_username"
} }
``` ```
@ -81,7 +90,7 @@ Output:
Command: Command:
``` ```
python sftpgo_api_cli.py update-user 5140 test_username --password "test_pwd" --home-dir="/tmp/test_home_dir" --uid 0 --gid 33 --max-sessions 3 --quota-size 0 --quota-files 4 --permissions "*" --upload-bandwidth 90 --download-bandwidth 80 --status 1 --expiration-date "" python sftpgo_api_cli.py update-user 9576 test_username --password "test_pwd" --home-dir="/tmp/test_home_dir" --uid 0 --gid 33 --max-sessions 3 --quota-size 0 --quota-files 4 --permissions "*" --subdirs-permissions "/dir1:list,download,create_symlinks" --upload-bandwidth 90 --download-bandwidth 80 --status 1 --expiration-date ""
``` ```
Output: Output:
@ -99,32 +108,39 @@ Output:
Command: Command:
``` ```
python sftpgo_api_cli.py get-user-by-id 5140 python sftpgo_api_cli.py get-user-by-id 9576
``` ```
Output: Output:
```json ```json
{ {
"id": 5140, "download_bandwidth": 80,
"username": "test_username", "expiration_date": 0,
"home_dir": "/tmp/test_home_dir",
"uid": 0,
"gid": 33, "gid": 33,
"max_sessions": 2, "home_dir": "/tmp/test_home_dir",
"quota_size": 0, "id": 9576,
"quota_files": 4, "last_login": 0,
"permissions": [ "last_quota_update": 0,
"max_sessions": 3,
"permissions": {
"/": [
"*" "*"
], ],
"used_quota_size": 0, "/dir1": [
"used_quota_files": 0, "list",
"last_quota_update": 0, "download",
"last_login": 0, "create_symlinks"
"expiration_date": 0, ]
},
"quota_files": 4,
"quota_size": 0,
"status": 1, "status": 1,
"uid": 0,
"upload_bandwidth": 90, "upload_bandwidth": 90,
"download_bandwidth": 80 "used_quota_files": 0,
"used_quota_size": 0,
"username": "test_username"
} }
``` ```
@ -141,25 +157,32 @@ Output:
```json ```json
[ [
{ {
"id": 5140, "download_bandwidth": 80,
"username": "test_username", "expiration_date": 0,
"home_dir": "/tmp/test_home_dir",
"uid": 0,
"gid": 33, "gid": 33,
"max_sessions": 2, "home_dir": "/tmp/test_home_dir",
"quota_size": 0, "id": 9576,
"quota_files": 4, "last_login": 0,
"permissions": [ "last_quota_update": 0,
"max_sessions": 3,
"permissions": {
"/": [
"*" "*"
], ],
"used_quota_size": 0, "/dir1": [
"used_quota_files": 0, "list",
"last_quota_update": 0, "download",
"last_login": 0, "create_symlinks"
"expiration_date": 0, ]
},
"quota_files": 4,
"quota_size": 0,
"status": 1, "status": 1,
"uid": 0,
"upload_bandwidth": 90, "upload_bandwidth": 90,
"download_bandwidth": 80 "used_quota_files": 0,
"used_quota_size": 0,
"username": "test_username"
} }
] ]
``` ```
@ -177,23 +200,23 @@ Output:
```json ```json
[ [
{ {
"username": "test_username",
"connection_id": "76a11b22260ee4249328df28bef34dc64c70f7c097db52159fc24049eeb0e32c",
"client_version": "SSH-2.0-OpenSSH_8.0",
"remote_address": "127.0.0.1:41622",
"connection_time": 1564696137971,
"last_activity": 1564696159605,
"protocol": "SFTP",
"ssh_command": "",
"active_transfers": [ "active_transfers": [
{ {
"last_activity": 1577197485561,
"operation_type": "upload", "operation_type": "upload",
"path": "/test_upload.gz", "path": "/test_upload.tar.gz",
"start_time": 1564696149783, "size": 1540096,
"size": 1146880, "start_time": 1577197471372
"last_activity": 1564696159605
} }
] ],
"client_version": "SSH-2.0-OpenSSH_8.1",
"connection_id": "f82cfec6a391ad673edd4ae9a144f32ccb59456139f8e1185b070134fffbab7c",
"connection_time": 1577197433003,
"last_activity": 1577197485561,
"protocol": "SFTP",
"remote_address": "127.0.0.1:43714",
"ssh_command": "",
"username": "test_username"
} }
] ]
``` ```
@ -203,7 +226,7 @@ Output:
Command: Command:
``` ```
python sftpgo_api_cli.py close-connection 76a11b22260ee4249328df28bef34dc64c70f7c097db52159fc24049eeb0e32c python sftpgo_api_cli.py close-connection f82cfec6a391ad673edd4ae9a144f32ccb59456139f8e1185b070134fffbab7c
``` ```
Output: Output:
@ -247,7 +270,7 @@ Output:
Command: Command:
``` ```
python sftpgo_api_cli.py delete-user 5140 python sftpgo_api_cli.py delete-user 9576
``` ```
Output: Output:
@ -272,9 +295,9 @@ Output:
```json ```json
{ {
"version": "0.9.0-dev", "build_date": "2019-12-24T14:17:47Z",
"build_date": "2019-08-08T08:11:34Z", "commit_hash": "f8fd5c0-dirty",
"commit_hash": "4f4489d-dirty" "version": "0.9.4-dev"
} }
``` ```

View file

@ -1,8 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
from datetime import datetime
import argparse import argparse
from datetime import datetime
import json import json
import requests import requests
try: try:
@ -60,7 +60,7 @@ class SFTPGoApiRequests:
print(r.text) print(r.text)
def buildUserObject(self, user_id=0, username="", password="", public_keys="", home_dir="", uid=0, def buildUserObject(self, user_id=0, username="", password="", public_keys="", home_dir="", uid=0,
gid=0, max_sessions=0, quota_size=0, quota_files=0, permissions=[], upload_bandwidth=0, gid=0, max_sessions=0, quota_size=0, quota_files=0, permissions={}, upload_bandwidth=0,
download_bandwidth=0, status=1, expiration_date=0): download_bandwidth=0, status=1, expiration_date=0):
user = {"id":user_id, "username":username, "uid":uid, "gid":gid, user = {"id":user_id, "username":username, "uid":uid, "gid":gid,
"max_sessions":max_sessions, "quota_size":quota_size, "quota_files":quota_files, "max_sessions":max_sessions, "quota_size":quota_size, "quota_files":quota_files,
@ -76,6 +76,23 @@ class SFTPGoApiRequests:
user.update({"permissions":permissions}) user.update({"permissions":permissions})
return user return user
def build_permissions(self, root_perms, subdirs_perms):
permissions = {}
if root_perms:
permissions.update({"/":root_perms})
for p in subdirs_perms:
if ":" in p:
directory = None
values = []
for value in p.split(":"):
if directory is None:
directory = value
else:
values = [v.strip() for v in value.split(",") if v.strip()]
if directory and values:
permissions.update({directory:values})
return permissions
def getUsers(self, limit=100, offset=0, order="ASC", username=""): def getUsers(self, limit=100, offset=0, order="ASC", username=""):
r = requests.get(self.userPath, params={"limit":limit, "offset":offset, "order":order, r = requests.get(self.userPath, params={"limit":limit, "offset":offset, "order":order,
"username":username}, auth=self.auth, verify=self.verify) "username":username}, auth=self.auth, verify=self.verify)
@ -86,18 +103,20 @@ class SFTPGoApiRequests:
self.printResponse(r) self.printResponse(r)
def addUser(self, username="", password="", public_keys="", home_dir="", uid=0, gid=0, max_sessions=0, def addUser(self, username="", password="", public_keys="", home_dir="", uid=0, gid=0, max_sessions=0,
quota_size=0, quota_files=0, permissions=[], upload_bandwidth=0, download_bandwidth=0, status=1, quota_size=0, quota_files=0, perms=[], upload_bandwidth=0, download_bandwidth=0, status=1,
expiration_date=0): expiration_date=0, subdirs_permissions=[]):
u = self.buildUserObject(0, username, password, public_keys, home_dir, uid, gid, max_sessions, u = self.buildUserObject(0, username, password, public_keys, home_dir, uid, gid, max_sessions,
quota_size, quota_files, permissions, upload_bandwidth, download_bandwidth, status, expiration_date) quota_size, quota_files, self.build_permissions(perms, subdirs_permissions), upload_bandwidth, download_bandwidth,
status, expiration_date)
r = requests.post(self.userPath, json=u, auth=self.auth, verify=self.verify) r = requests.post(self.userPath, json=u, auth=self.auth, verify=self.verify)
self.printResponse(r) self.printResponse(r)
def updateUser(self, user_id, username="", password="", public_keys="", home_dir="", uid=0, gid=0, def updateUser(self, user_id, username="", password="", public_keys="", home_dir="", uid=0, gid=0,
max_sessions=0, quota_size=0, quota_files=0, permissions=[], upload_bandwidth=0, max_sessions=0, quota_size=0, quota_files=0, perms=[], upload_bandwidth=0,
download_bandwidth=0, status=1, expiration_date=0): download_bandwidth=0, status=1, expiration_date=0, subdirs_permissions=[]):
u = self.buildUserObject(user_id, username, password, public_keys, home_dir, uid, gid, max_sessions, u = self.buildUserObject(user_id, username, password, public_keys, home_dir, uid, gid, max_sessions,
quota_size, quota_files, permissions, upload_bandwidth, download_bandwidth, status, expiration_date) quota_size, quota_files, self.build_permissions(perms, subdirs_permissions), upload_bandwidth, download_bandwidth,
status, expiration_date)
r = requests.put(urlparse.urljoin(self.userPath, "user/" + str(user_id)), json=u, auth=self.auth, verify=self.verify) r = requests.put(urlparse.urljoin(self.userPath, "user/" + str(user_id)), json=u, auth=self.auth, verify=self.verify)
self.printResponse(r) self.printResponse(r)
@ -160,7 +179,10 @@ def addCommonUserArguments(parser):
parser.add_argument('-F', '--quota-files', type=int, default=0, help="default: %(default)s") parser.add_argument('-F', '--quota-files', type=int, default=0, help="default: %(default)s")
parser.add_argument('-G', '--permissions', type=str, nargs='+', default=[], parser.add_argument('-G', '--permissions', type=str, nargs='+', default=[],
choices=['*', 'list', 'download', 'upload', 'overwrite', 'delete', 'rename', 'create_dirs', choices=['*', 'list', 'download', 'upload', 'overwrite', 'delete', 'rename', 'create_dirs',
'create_symlinks', 'chmod', 'chown', 'chtimes'], help='Default: %(default)s') 'create_symlinks', 'chmod', 'chown', 'chtimes'], help='Permissions for the root directory '
+'(/). Default: %(default)s')
parser.add_argument('--subdirs-permissions', type=str, nargs='*', default=[], help='Permissions for subdirs. '
+'For example: "/somedir:list,download" "/otherdir/subdir:*" Default: %(default)s')
parser.add_argument('-U', '--upload-bandwidth', type=int, default=0, parser.add_argument('-U', '--upload-bandwidth', type=int, default=0,
help='Maximum upload bandwidth as KB/s, 0 means unlimited. Default: %(default)s') help='Maximum upload bandwidth as KB/s, 0 means unlimited. Default: %(default)s')
parser.add_argument('-D', '--download-bandwidth', type=int, default=0, parser.add_argument('-D', '--download-bandwidth', type=int, default=0,
@ -237,11 +259,12 @@ if __name__ == '__main__':
if args.command == 'add-user': if args.command == 'add-user':
api.addUser(args.username, args.password, args.public_keys, args.home_dir, args.uid, args.gid, args.max_sessions, api.addUser(args.username, args.password, args.public_keys, args.home_dir, args.uid, args.gid, args.max_sessions,
args.quota_size, args.quota_files, args.permissions, args.upload_bandwidth, args.download_bandwidth, args.quota_size, args.quota_files, args.permissions, args.upload_bandwidth, args.download_bandwidth,
args.status, getDatetimeAsMillisSinceEpoch(args.expiration_date)) args.status, getDatetimeAsMillisSinceEpoch(args.expiration_date), args.subdirs_permissions)
elif args.command == 'update-user': elif args.command == 'update-user':
api.updateUser(args.id, args.username, args.password, args.public_keys, args.home_dir, args.uid, args.gid, api.updateUser(args.id, args.username, args.password, args.public_keys, args.home_dir, args.uid, args.gid,
args.max_sessions, args.quota_size, args.quota_files, args.permissions, args.upload_bandwidth, args.max_sessions, args.quota_size, args.quota_files, args.permissions, args.upload_bandwidth,
args.download_bandwidth, args.status, getDatetimeAsMillisSinceEpoch(args.expiration_date)) args.download_bandwidth, args.status, getDatetimeAsMillisSinceEpoch(args.expiration_date),
args.subdirs_permissions)
elif args.command == 'delete-user': elif args.command == 'delete-user':
api.deleteUser(args.id) api.deleteUser(args.id)
elif args.command == 'get-users': elif args.command == 'get-users':

View file

@ -51,14 +51,13 @@ func (c Connection) Log(level logger.LogLevel, sender string, format string, v .
func (c Connection) Fileread(request *sftp.Request) (io.ReaderAt, error) { func (c Connection) Fileread(request *sftp.Request) (io.ReaderAt, error) {
updateConnectionActivity(c.ID) updateConnectionActivity(c.ID)
if !c.User.HasPerm(dataprovider.PermDownload) {
return nil, sftp.ErrSSHFxPermissionDenied
}
p, err := c.buildPath(request.Filepath) p, err := c.buildPath(request.Filepath)
if err != nil { if err != nil {
return nil, getSFTPErrorFromOSError(err) return nil, getSFTPErrorFromOSError(err)
} }
if !c.User.HasPerm(dataprovider.PermDownload, filepath.Dir(p)) {
return nil, sftp.ErrSSHFxPermissionDenied
}
c.lock.Lock() c.lock.Lock()
defer c.lock.Unlock() defer c.lock.Unlock()
@ -98,10 +97,6 @@ func (c Connection) Fileread(request *sftp.Request) (io.ReaderAt, error) {
// Filewrite handles the write actions for a file on the system. // Filewrite handles the write actions for a file on the system.
func (c Connection) Filewrite(request *sftp.Request) (io.WriterAt, error) { func (c Connection) Filewrite(request *sftp.Request) (io.WriterAt, error) {
updateConnectionActivity(c.ID) updateConnectionActivity(c.ID)
if !c.User.HasPerm(dataprovider.PermUpload) {
return nil, sftp.ErrSSHFxPermissionDenied
}
p, err := c.buildPath(request.Filepath) p, err := c.buildPath(request.Filepath)
if err != nil { if err != nil {
return nil, getSFTPErrorFromOSError(err) return nil, getSFTPErrorFromOSError(err)
@ -119,6 +114,9 @@ func (c Connection) Filewrite(request *sftp.Request) (io.WriterAt, error) {
// If the file doesn't exist we need to create it, as well as the directory pathway // If the file doesn't exist we need to create it, as well as the directory pathway
// leading up to where that file will be created. // leading up to where that file will be created.
if os.IsNotExist(statErr) { if os.IsNotExist(statErr) {
if !c.User.HasPerm(dataprovider.PermUpload, filepath.Dir(p)) {
return nil, sftp.ErrSSHFxPermissionDenied
}
return c.handleSFTPUploadToNewFile(p, filePath) return c.handleSFTPUploadToNewFile(p, filePath)
} }
@ -133,7 +131,7 @@ func (c Connection) Filewrite(request *sftp.Request) (io.WriterAt, error) {
return nil, sftp.ErrSSHFxOpUnsupported return nil, sftp.ErrSSHFxOpUnsupported
} }
if !c.User.HasPerm(dataprovider.PermOverwrite) { if !c.User.HasPerm(dataprovider.PermOverwrite, filepath.Dir(filePath)) {
return nil, sftp.ErrSSHFxPermissionDenied return nil, sftp.ErrSSHFxPermissionDenied
} }
@ -212,7 +210,7 @@ func (c Connection) Filelist(request *sftp.Request) (sftp.ListerAt, error) {
switch request.Method { switch request.Method {
case "List": case "List":
if !c.User.HasPerm(dataprovider.PermListItems) { if !c.User.HasPerm(dataprovider.PermListItems, p) {
return nil, sftp.ErrSSHFxPermissionDenied return nil, sftp.ErrSSHFxPermissionDenied
} }
@ -226,7 +224,7 @@ func (c Connection) Filelist(request *sftp.Request) (sftp.ListerAt, error) {
return listerAt(files), nil return listerAt(files), nil
case "Stat": case "Stat":
if !c.User.HasPerm(dataprovider.PermListItems) { if !c.User.HasPerm(dataprovider.PermListItems, filepath.Dir(p)) {
return nil, sftp.ErrSSHFxPermissionDenied return nil, sftp.ErrSSHFxPermissionDenied
} }
@ -266,9 +264,15 @@ func (c Connection) handleSFTPSetstat(path string, request *sftp.Request) error
c.ClientVersion) c.ClientVersion)
return sftp.ErrSSHFxBadMessage return sftp.ErrSSHFxBadMessage
} }
pathForPerms := path
if fi, err := os.Lstat(path); err == nil {
if fi.IsDir() {
pathForPerms = filepath.Dir(path)
}
}
attrFlags := request.AttrFlags() attrFlags := request.AttrFlags()
if attrFlags.Permissions { if attrFlags.Permissions {
if !c.User.HasPerm(dataprovider.PermChmod) { if !c.User.HasPerm(dataprovider.PermChmod, pathForPerms) {
return sftp.ErrSSHFxPermissionDenied return sftp.ErrSSHFxPermissionDenied
} }
fileMode := request.Attributes().FileMode() fileMode := request.Attributes().FileMode()
@ -279,7 +283,7 @@ func (c Connection) handleSFTPSetstat(path string, request *sftp.Request) error
logger.CommandLog(chmodLogSender, path, "", c.User.Username, fileMode.String(), c.ID, c.protocol, -1, -1, "", "", "") logger.CommandLog(chmodLogSender, path, "", c.User.Username, fileMode.String(), c.ID, c.protocol, -1, -1, "", "", "")
return nil return nil
} else if attrFlags.UidGid { } else if attrFlags.UidGid {
if !c.User.HasPerm(dataprovider.PermChown) { if !c.User.HasPerm(dataprovider.PermChown, pathForPerms) {
return sftp.ErrSSHFxPermissionDenied return sftp.ErrSSHFxPermissionDenied
} }
uid := int(request.Attributes().UID) uid := int(request.Attributes().UID)
@ -291,7 +295,7 @@ func (c Connection) handleSFTPSetstat(path string, request *sftp.Request) error
logger.CommandLog(chownLogSender, path, "", c.User.Username, "", c.ID, c.protocol, uid, gid, "", "", "") logger.CommandLog(chownLogSender, path, "", c.User.Username, "", c.ID, c.protocol, uid, gid, "", "", "")
return nil return nil
} else if attrFlags.Acmodtime { } else if attrFlags.Acmodtime {
if !c.User.HasPerm(dataprovider.PermChtimes) { if !c.User.HasPerm(dataprovider.PermChtimes, pathForPerms) {
return sftp.ErrSSHFxPermissionDenied return sftp.ErrSSHFxPermissionDenied
} }
dateFormat := "2006-01-02T15:04:05" // YYYY-MM-DDTHH:MM:SS dateFormat := "2006-01-02T15:04:05" // YYYY-MM-DDTHH:MM:SS
@ -312,7 +316,7 @@ func (c Connection) handleSFTPSetstat(path string, request *sftp.Request) error
} }
func (c Connection) handleSFTPRename(sourcePath string, targetPath string) error { func (c Connection) handleSFTPRename(sourcePath string, targetPath string) error {
if !c.User.HasPerm(dataprovider.PermRename) { if !c.User.HasPerm(dataprovider.PermRename, filepath.Dir(targetPath)) {
return sftp.ErrSSHFxPermissionDenied return sftp.ErrSSHFxPermissionDenied
} }
if err := os.Rename(sourcePath, targetPath); err != nil { if err := os.Rename(sourcePath, targetPath); err != nil {
@ -325,7 +329,7 @@ func (c Connection) handleSFTPRename(sourcePath string, targetPath string) error
} }
func (c Connection) handleSFTPRmdir(path string) error { func (c Connection) handleSFTPRmdir(path string) error {
if !c.User.HasPerm(dataprovider.PermDelete) { if !c.User.HasPerm(dataprovider.PermDelete, filepath.Dir(path)) {
return sftp.ErrSSHFxPermissionDenied return sftp.ErrSSHFxPermissionDenied
} }
@ -350,7 +354,7 @@ func (c Connection) handleSFTPRmdir(path string) error {
} }
func (c Connection) handleSFTPSymlink(sourcePath string, targetPath string) error { func (c Connection) handleSFTPSymlink(sourcePath string, targetPath string) error {
if !c.User.HasPerm(dataprovider.PermCreateSymlinks) { if !c.User.HasPerm(dataprovider.PermCreateSymlinks, filepath.Dir(targetPath)) {
return sftp.ErrSSHFxPermissionDenied return sftp.ErrSSHFxPermissionDenied
} }
if err := os.Symlink(sourcePath, targetPath); err != nil { if err := os.Symlink(sourcePath, targetPath); err != nil {
@ -363,7 +367,7 @@ func (c Connection) handleSFTPSymlink(sourcePath string, targetPath string) erro
} }
func (c Connection) handleSFTPMkdir(path string) error { func (c Connection) handleSFTPMkdir(path string) error {
if !c.User.HasPerm(dataprovider.PermCreateDirs) { if !c.User.HasPerm(dataprovider.PermCreateDirs, filepath.Dir(path)) {
return sftp.ErrSSHFxPermissionDenied return sftp.ErrSSHFxPermissionDenied
} }
if err := os.Mkdir(path, 0777); err != nil { if err := os.Mkdir(path, 0777); err != nil {
@ -377,7 +381,7 @@ func (c Connection) handleSFTPMkdir(path string) error {
} }
func (c Connection) handleSFTPRemove(path string) error { func (c Connection) handleSFTPRemove(path string) error {
if !c.User.HasPerm(dataprovider.PermDelete) { if !c.User.HasPerm(dataprovider.PermDelete, filepath.Dir(path)) {
return sftp.ErrSSHFxPermissionDenied return sftp.ErrSSHFxPermissionDenied
} }

View file

@ -192,7 +192,8 @@ func TestSFTPCmdTargetPath(t *testing.T) {
u := dataprovider.User{} u := dataprovider.User{}
u.HomeDir = "home_rel_path" u.HomeDir = "home_rel_path"
u.Username = "test" u.Username = "test"
u.Permissions = []string{"*"} u.Permissions = make(map[string][]string)
u.Permissions["/"] = []string{dataprovider.PermAny}
connection := Connection{ connection := Connection{
User: u, User: u,
} }
@ -242,7 +243,8 @@ func TestSFTPGetUsedQuota(t *testing.T) {
u.Username = "test_invalid_user" u.Username = "test_invalid_user"
u.QuotaSize = 4096 u.QuotaSize = 4096
u.QuotaFiles = 1 u.QuotaFiles = 1
u.Permissions = []string{"*"} u.Permissions = make(map[string][]string)
u.Permissions["/"] = []string{dataprovider.PermAny}
connection := Connection{ connection := Connection{
User: u, User: u,
} }
@ -323,12 +325,13 @@ func TestSSHCommandErrors(t *testing.T) {
server, client := net.Pipe() server, client := net.Pipe()
defer server.Close() defer server.Close()
defer client.Close() defer client.Close()
user := dataprovider.User{}
user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermAny}
connection := Connection{ connection := Connection{
channel: &mockSSHChannel, channel: &mockSSHChannel,
netConn: client, netConn: client,
User: dataprovider.User{ User: user,
Permissions: []string{dataprovider.PermAny},
},
} }
cmd := sshCommand{ cmd := sshCommand{
command: "md5sum", command: "md5sum",
@ -366,12 +369,13 @@ func TestSSHCommandErrors(t *testing.T) {
} }
cmd.connection.User.QuotaFiles = 0 cmd.connection.User.QuotaFiles = 0
cmd.connection.User.UsedQuotaFiles = 0 cmd.connection.User.UsedQuotaFiles = 0
cmd.connection.User.Permissions = []string{dataprovider.PermListItems} cmd.connection.User.Permissions = make(map[string][]string)
cmd.connection.User.Permissions["/"] = []string{dataprovider.PermListItems}
err = cmd.handle() err = cmd.handle()
if err != errPermissionDenied { if err != errPermissionDenied {
t.Errorf("unexpected error: %v", err) t.Errorf("unexpected error: %v", err)
} }
cmd.connection.User.Permissions = []string{dataprovider.PermAny} cmd.connection.User.Permissions["/"] = []string{dataprovider.PermAny}
cmd.command = "invalid_command" cmd.command = "invalid_command"
command, err := cmd.getSystemCommand() command, err := cmd.getSystemCommand()
if err != nil { if err != nil {
@ -417,11 +421,13 @@ func TestSSHCommandQuotaScan(t *testing.T) {
server, client := net.Pipe() server, client := net.Pipe()
defer server.Close() defer server.Close()
defer client.Close() defer client.Close()
permissions := make(map[string][]string)
permissions["/"] = []string{dataprovider.PermAny}
connection := Connection{ connection := Connection{
channel: &mockSSHChannel, channel: &mockSSHChannel,
netConn: client, netConn: client,
User: dataprovider.User{ User: dataprovider.User{
Permissions: []string{dataprovider.PermAny}, Permissions: permissions,
QuotaFiles: 1, QuotaFiles: 1,
HomeDir: "invalid_path", HomeDir: "invalid_path",
}, },
@ -438,9 +444,11 @@ func TestSSHCommandQuotaScan(t *testing.T) {
} }
func TestRsyncOptions(t *testing.T) { func TestRsyncOptions(t *testing.T) {
permissions := make(map[string][]string)
permissions["/"] = []string{dataprovider.PermAny}
conn := Connection{ conn := Connection{
User: dataprovider.User{ User: dataprovider.User{
Permissions: []string{dataprovider.PermAny}, Permissions: permissions,
HomeDir: os.TempDir(), HomeDir: os.TempDir(),
}, },
} }
@ -456,10 +464,11 @@ func TestRsyncOptions(t *testing.T) {
if !utils.IsStringInSlice("--safe-links", cmd.cmd.Args) { if !utils.IsStringInSlice("--safe-links", cmd.cmd.Args) {
t.Errorf("--safe-links must be added if the user has the create symlinks permission") t.Errorf("--safe-links must be added if the user has the create symlinks permission")
} }
permissions["/"] = []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermCreateDirs,
dataprovider.PermListItems, dataprovider.PermOverwrite, dataprovider.PermDelete, dataprovider.PermRename}
conn = Connection{ conn = Connection{
User: dataprovider.User{ User: dataprovider.User{
Permissions: []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermCreateDirs, Permissions: permissions,
dataprovider.PermListItems, dataprovider.PermOverwrite, dataprovider.PermDelete, dataprovider.PermRename},
HomeDir: os.TempDir(), HomeDir: os.TempDir(),
}, },
} }
@ -491,18 +500,20 @@ func TestSystemCommandErrors(t *testing.T) {
server, client := net.Pipe() server, client := net.Pipe()
defer server.Close() defer server.Close()
defer client.Close() defer client.Close()
permissions := make(map[string][]string)
permissions["/"] = []string{dataprovider.PermAny}
connection := Connection{ connection := Connection{
channel: &mockSSHChannel, channel: &mockSSHChannel,
netConn: client, netConn: client,
User: dataprovider.User{ User: dataprovider.User{
Permissions: []string{dataprovider.PermAny}, Permissions: permissions,
HomeDir: os.TempDir(), HomeDir: os.TempDir(),
}, },
} }
sshCmd := sshCommand{ sshCmd := sshCommand{
command: "ls", command: "ls",
connection: connection, connection: connection,
args: []string{}, args: []string{"/"},
} }
systemCmd, err := sshCmd.getSystemCommand() systemCmd, err := sshCmd.getSystemCommand()
if err != nil { if err != nil {
@ -929,7 +940,8 @@ func TestSCPCreateDirs(t *testing.T) {
u := dataprovider.User{} u := dataprovider.User{}
u.HomeDir = "home_rel_path" u.HomeDir = "home_rel_path"
u.Username = "test" u.Username = "test"
u.Permissions = []string{"*"} u.Permissions = make(map[string][]string)
u.Permissions["/"] = []string{dataprovider.PermAny}
mockSSHChannel := MockChannel{ mockSSHChannel := MockChannel{
Buffer: bytes.NewBuffer(buf), Buffer: bytes.NewBuffer(buf),
StdErrBuffer: bytes.NewBuffer(stdErrBuf), StdErrBuffer: bytes.NewBuffer(stdErrBuf),

View file

@ -114,19 +114,18 @@ func (c *scpCommand) handleRecursiveUpload() error {
func (c *scpCommand) handleCreateDir(dirPath string) error { func (c *scpCommand) handleCreateDir(dirPath string) error {
updateConnectionActivity(c.connection.ID) updateConnectionActivity(c.connection.ID)
if !c.connection.User.HasPerm(dataprovider.PermCreateDirs) {
err := fmt.Errorf("Permission denied")
c.connection.Log(logger.LevelWarn, logSenderSCP, "error creating dir: %#v, permission denied", dirPath)
c.sendErrorMessage(err.Error())
return err
}
p, err := c.connection.buildPath(dirPath) p, err := c.connection.buildPath(dirPath)
if err != nil { if err != nil {
c.connection.Log(logger.LevelWarn, logSenderSCP, "error creating dir: %#v, invalid file path, err: %v", dirPath, err) c.connection.Log(logger.LevelWarn, logSenderSCP, "error creating dir: %#v, invalid file path, err: %v", dirPath, err)
c.sendErrorMessage(err.Error()) c.sendErrorMessage(err.Error())
return err return err
} }
if !c.connection.User.HasPerm(dataprovider.PermCreateDirs, filepath.Dir(p)) {
err := fmt.Errorf("Permission denied")
c.connection.Log(logger.LevelWarn, logSenderSCP, "error creating dir: %#v, permission denied", dirPath)
c.sendErrorMessage(err.Error())
return err
}
err = c.createDir(p) err = c.createDir(p)
if err != nil { if err != nil {
@ -188,15 +187,6 @@ func (c *scpCommand) handleUploadFile(requestPath, filePath string, sizeToRead i
return err return err
} }
if _, err := os.Stat(filepath.Dir(requestPath)); os.IsNotExist(err) {
if !c.connection.User.HasPerm(dataprovider.PermCreateDirs) {
err := fmt.Errorf("Permission denied")
c.connection.Log(logger.LevelWarn, logSenderSCP, "error uploading file: %#v, permission denied", requestPath)
c.sendErrorMessage(err.Error())
return err
}
}
file, err := os.Create(filePath) file, err := os.Create(filePath)
if err != nil { if err != nil {
c.connection.Log(logger.LevelError, logSenderSCP, "error creating file %#v: %v", requestPath, err) c.connection.Log(logger.LevelError, logSenderSCP, "error creating file %#v: %v", requestPath, err)
@ -231,12 +221,6 @@ func (c *scpCommand) handleUpload(uploadFilePath string, sizeToRead int64) error
var err error var err error
updateConnectionActivity(c.connection.ID) updateConnectionActivity(c.connection.ID)
if !c.connection.User.HasPerm(dataprovider.PermUpload) {
err := fmt.Errorf("Permission denied")
c.connection.Log(logger.LevelWarn, logSenderSCP, "cannot upload file: %#v, permission denied", uploadFilePath)
c.sendErrorMessage(err.Error())
return err
}
p, err := c.connection.buildPath(uploadFilePath) p, err := c.connection.buildPath(uploadFilePath)
if err != nil { if err != nil {
@ -250,6 +234,12 @@ func (c *scpCommand) handleUpload(uploadFilePath string, sizeToRead int64) error
} }
stat, statErr := os.Stat(p) stat, statErr := os.Stat(p)
if os.IsNotExist(statErr) { if os.IsNotExist(statErr) {
if !c.connection.User.HasPerm(dataprovider.PermUpload, filepath.Dir(p)) {
err := fmt.Errorf("Permission denied")
c.connection.Log(logger.LevelWarn, logSenderSCP, "cannot upload file: %#v, permission denied", uploadFilePath)
c.sendErrorMessage(err.Error())
return err
}
return c.handleUploadFile(p, filePath, sizeToRead, true) return c.handleUploadFile(p, filePath, sizeToRead, true)
} }
@ -266,7 +256,7 @@ func (c *scpCommand) handleUpload(uploadFilePath string, sizeToRead int64) error
return err return err
} }
if !c.connection.User.HasPerm(dataprovider.PermOverwrite) { if !c.connection.User.HasPerm(dataprovider.PermOverwrite, filePath) {
err := fmt.Errorf("Permission denied") err := fmt.Errorf("Permission denied")
c.connection.Log(logger.LevelWarn, logSenderSCP, "cannot overwrite file: %#v, permission denied", uploadFilePath) c.connection.Log(logger.LevelWarn, logSenderSCP, "cannot overwrite file: %#v, permission denied", uploadFilePath)
c.sendErrorMessage(err.Error()) c.sendErrorMessage(err.Error())
@ -425,12 +415,6 @@ func (c *scpCommand) handleDownload(filePath string) error {
updateConnectionActivity(c.connection.ID) updateConnectionActivity(c.connection.ID)
if !c.connection.User.HasPerm(dataprovider.PermDownload) {
err := fmt.Errorf("Permission denied")
c.connection.Log(logger.LevelWarn, logSenderSCP, "error downloading file: %#v, permission denied", filePath)
c.sendErrorMessage(err.Error())
return err
}
p, err := c.connection.buildPath(filePath) p, err := c.connection.buildPath(filePath)
if err != nil { if err != nil {
err := fmt.Errorf("Invalid file path") err := fmt.Errorf("Invalid file path")
@ -447,10 +431,23 @@ func (c *scpCommand) handleDownload(filePath string) error {
} }
if stat.IsDir() { if stat.IsDir() {
if !c.connection.User.HasPerm(dataprovider.PermDownload, p) {
err := fmt.Errorf("Permission denied")
c.connection.Log(logger.LevelWarn, logSenderSCP, "error downloading dir: %#v, permission denied", filePath)
c.sendErrorMessage(err.Error())
return err
}
err = c.handleRecursiveDownload(p, stat) err = c.handleRecursiveDownload(p, stat)
return err return err
} }
if !c.connection.User.HasPerm(dataprovider.PermDownload, filepath.Dir(p)) {
err := fmt.Errorf("Permission denied")
c.connection.Log(logger.LevelWarn, logSenderSCP, "error downloading dir: %#v, permission denied", filePath)
c.sendErrorMessage(err.Error())
return err
}
file, err := os.Open(p) file, err := os.Open(p)
if err != nil { if err != nil {
c.connection.Log(logger.LevelError, logSenderSCP, "could not open file %#v for reading: %v", p, err) c.connection.Log(logger.LevelError, logSenderSCP, "could not open file %#v for reading: %v", p, err)

View file

@ -196,7 +196,7 @@ func TestMain(m *testing.M) {
waitTCPListening(fmt.Sprintf("%s:%d", httpdConf.BindAddress, httpdConf.BindPort)) waitTCPListening(fmt.Sprintf("%s:%d", httpdConf.BindAddress, httpdConf.BindPort))
exitCode := m.Run() exitCode := m.Run()
//os.Remove(logfilePath) os.Remove(logfilePath)
os.Remove(loginBannerFile) os.Remove(loginBannerFile)
os.Remove(pubKeyPath) os.Remove(pubKeyPath)
os.Remove(privateKeyPath) os.Remove(privateKeyPath)
@ -1395,6 +1395,7 @@ func TestMissingFile(t *testing.T) {
if err == nil { if err == nil {
t.Errorf("download missing file must fail") t.Errorf("download missing file must fail")
} }
os.Remove(localDownloadPath)
} }
_, err = httpd.RemoveUser(user, http.StatusOK) _, err = httpd.RemoveUser(user, http.StatusOK)
if err != nil { if err != nil {
@ -1697,7 +1698,7 @@ func TestPasswordsHashSHA512Crypt(t *testing.T) {
func TestPermList(t *testing.T) { func TestPermList(t *testing.T) {
usePubKey := true usePubKey := true
u := getTestUser(usePubKey) u := getTestUser(usePubKey)
u.Permissions = []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename, u.Permissions["/"] = []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename,
dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod,
dataprovider.PermChown, dataprovider.PermChtimes} dataprovider.PermChown, dataprovider.PermChtimes}
user, _, err := httpd.AddUser(u, http.StatusOK) user, _, err := httpd.AddUser(u, http.StatusOK)
@ -1728,7 +1729,7 @@ func TestPermList(t *testing.T) {
func TestPermDownload(t *testing.T) { func TestPermDownload(t *testing.T) {
usePubKey := true usePubKey := true
u := getTestUser(usePubKey) u := getTestUser(usePubKey)
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename, u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename,
dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod,
dataprovider.PermChown, dataprovider.PermChtimes} dataprovider.PermChown, dataprovider.PermChtimes}
user, _, err := httpd.AddUser(u, http.StatusOK) user, _, err := httpd.AddUser(u, http.StatusOK)
@ -1773,7 +1774,7 @@ func TestPermDownload(t *testing.T) {
func TestPermUpload(t *testing.T) { func TestPermUpload(t *testing.T) {
usePubKey := false usePubKey := false
u := getTestUser(usePubKey) u := getTestUser(usePubKey)
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermDelete, dataprovider.PermRename, u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermDelete, dataprovider.PermRename,
dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod,
dataprovider.PermChown, dataprovider.PermChtimes} dataprovider.PermChown, dataprovider.PermChtimes}
user, _, err := httpd.AddUser(u, http.StatusOK) user, _, err := httpd.AddUser(u, http.StatusOK)
@ -1808,7 +1809,7 @@ func TestPermUpload(t *testing.T) {
func TestPermOverwrite(t *testing.T) { func TestPermOverwrite(t *testing.T) {
usePubKey := false usePubKey := false
u := getTestUser(usePubKey) u := getTestUser(usePubKey)
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,
dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermChmod, dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermChmod,
dataprovider.PermChown, dataprovider.PermChtimes} dataprovider.PermChown, dataprovider.PermChtimes}
user, _, err := httpd.AddUser(u, http.StatusOK) user, _, err := httpd.AddUser(u, http.StatusOK)
@ -1847,7 +1848,7 @@ func TestPermOverwrite(t *testing.T) {
func TestPermDelete(t *testing.T) { func TestPermDelete(t *testing.T) {
usePubKey := false usePubKey := false
u := getTestUser(usePubKey) u := getTestUser(usePubKey)
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermRename, u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermRename,
dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod,
dataprovider.PermChown, dataprovider.PermChtimes} dataprovider.PermChown, dataprovider.PermChtimes}
user, _, err := httpd.AddUser(u, http.StatusOK) user, _, err := httpd.AddUser(u, http.StatusOK)
@ -1886,7 +1887,7 @@ func TestPermDelete(t *testing.T) {
func TestPermRename(t *testing.T) { func TestPermRename(t *testing.T) {
usePubKey := false usePubKey := false
u := getTestUser(usePubKey) u := getTestUser(usePubKey)
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,
dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod,
dataprovider.PermChown, dataprovider.PermChtimes} dataprovider.PermChown, dataprovider.PermChtimes}
user, _, err := httpd.AddUser(u, http.StatusOK) user, _, err := httpd.AddUser(u, http.StatusOK)
@ -1929,7 +1930,7 @@ func TestPermRename(t *testing.T) {
func TestPermCreateDirs(t *testing.T) { func TestPermCreateDirs(t *testing.T) {
usePubKey := false usePubKey := false
u := getTestUser(usePubKey) u := getTestUser(usePubKey)
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,
dataprovider.PermRename, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermRename, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermChmod,
dataprovider.PermChown, dataprovider.PermChtimes} dataprovider.PermChown, dataprovider.PermChtimes}
user, _, err := httpd.AddUser(u, http.StatusOK) user, _, err := httpd.AddUser(u, http.StatusOK)
@ -1956,7 +1957,7 @@ func TestPermCreateDirs(t *testing.T) {
func TestPermSymlink(t *testing.T) { func TestPermSymlink(t *testing.T) {
usePubKey := false usePubKey := false
u := getTestUser(usePubKey) u := getTestUser(usePubKey)
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,
dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermChown, dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermOverwrite, dataprovider.PermChmod, dataprovider.PermChown,
dataprovider.PermChtimes} dataprovider.PermChtimes}
user, _, err := httpd.AddUser(u, http.StatusOK) user, _, err := httpd.AddUser(u, http.StatusOK)
@ -1999,7 +2000,7 @@ func TestPermSymlink(t *testing.T) {
func TestPermChmod(t *testing.T) { func TestPermChmod(t *testing.T) {
usePubKey := false usePubKey := false
u := getTestUser(usePubKey) u := getTestUser(usePubKey)
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,
dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite,
dataprovider.PermChown, dataprovider.PermChtimes} dataprovider.PermChown, dataprovider.PermChtimes}
user, _, err := httpd.AddUser(u, http.StatusOK) user, _, err := httpd.AddUser(u, http.StatusOK)
@ -2042,7 +2043,7 @@ func TestPermChmod(t *testing.T) {
func TestPermChown(t *testing.T) { func TestPermChown(t *testing.T) {
usePubKey := false usePubKey := false
u := getTestUser(usePubKey) u := getTestUser(usePubKey)
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,
dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite,
dataprovider.PermChmod, dataprovider.PermChtimes} dataprovider.PermChmod, dataprovider.PermChtimes}
user, _, err := httpd.AddUser(u, http.StatusOK) user, _, err := httpd.AddUser(u, http.StatusOK)
@ -2085,7 +2086,7 @@ func TestPermChown(t *testing.T) {
func TestPermChtimes(t *testing.T) { func TestPermChtimes(t *testing.T) {
usePubKey := false usePubKey := false
u := getTestUser(usePubKey) u := getTestUser(usePubKey)
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,
dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite, dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite,
dataprovider.PermChmod, dataprovider.PermChown} dataprovider.PermChmod, dataprovider.PermChown}
user, _, err := httpd.AddUser(u, http.StatusOK) user, _, err := httpd.AddUser(u, http.StatusOK)
@ -2125,6 +2126,396 @@ func TestPermChtimes(t *testing.T) {
os.RemoveAll(user.GetHomeDir()) os.RemoveAll(user.GetHomeDir())
} }
func TestSubDirsUploads(t *testing.T) {
usePubKey := true
u := getTestUser(usePubKey)
u.Permissions["/"] = []string{dataprovider.PermAny}
u.Permissions["/subdir"] = []string{dataprovider.PermChtimes, dataprovider.PermDownload}
user, _, err := httpd.AddUser(u, http.StatusOK)
if err != nil {
t.Errorf("unable to add user: %v", err)
}
client, err := getSftpClient(user, usePubKey)
if err != nil {
t.Errorf("unable to create sftp client: %v", err)
} else {
defer client.Close()
err = client.Mkdir("subdir")
if err != nil {
t.Errorf("unexpected mkdir error: %v", err)
}
testFileName := "test_file.dat"
testFileNameSub := "/subdir/test_file_dat"
testFilePath := filepath.Join(homeBasePath, testFileName)
testFileSize := int64(65535)
err = createTestFile(testFilePath, testFileSize)
if err != nil {
t.Errorf("unable to create test file: %v", err)
}
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
if err != nil {
t.Errorf("file upload error: %v", err)
}
err = sftpUploadFile(testFilePath, testFileNameSub, testFileSize, client)
if !strings.Contains(err.Error(), "Permission Denied") {
t.Errorf("unexpected upload error: %v", err)
}
err = client.Symlink(testFileName, testFileNameSub+".link")
if !strings.Contains(err.Error(), "Permission Denied") {
t.Errorf("unexpected upload error: %v", err)
}
err = client.Symlink(testFileName, testFileName+".link")
if err != nil {
t.Errorf("symlink error: %v", err)
}
err = client.Rename(testFileName, testFileNameSub+".rename")
if !strings.Contains(err.Error(), "Permission Denied") {
t.Errorf("unexpected rename error: %v", err)
}
err = client.Rename(testFileName, testFileName+".rename")
if err != nil {
t.Errorf("rename error: %v", err)
}
err = client.Remove(testFileNameSub)
if !strings.Contains(err.Error(), "Permission Denied") {
t.Errorf("unexpected upload error: %v", err)
}
err = client.Remove(testFileName + ".rename")
if err != nil {
t.Errorf("remove error: %v", err)
}
os.Remove(testFilePath)
}
httpd.RemoveUser(user, http.StatusOK)
os.RemoveAll(user.GetHomeDir())
}
func TestSubDirsOverwrite(t *testing.T) {
usePubKey := true
u := getTestUser(usePubKey)
u.Permissions["/"] = []string{dataprovider.PermAny}
u.Permissions["/subdir"] = []string{dataprovider.PermOverwrite, dataprovider.PermListItems}
user, _, err := httpd.AddUser(u, http.StatusOK)
if err != nil {
t.Errorf("unable to add user: %v", err)
}
client, err := getSftpClient(user, usePubKey)
if err != nil {
t.Errorf("unable to create sftp client: %v", err)
} else {
defer client.Close()
testFileName := "/subdir/test_file.dat"
testFilePath := filepath.Join(homeBasePath, "test_file.dat")
testFileSFTPPath := filepath.Join(u.GetHomeDir(), "subdir", "test_file.dat")
testFileSize := int64(65535)
err = createTestFile(testFilePath, testFileSize)
if err != nil {
t.Errorf("unable to create test file: %v", err)
}
err = createTestFile(testFileSFTPPath, 16384)
if err != nil {
t.Errorf("unable to create test file: %v", err)
}
err = sftpUploadFile(testFilePath, testFileName+".new", testFileSize, client)
if !strings.Contains(err.Error(), "Permission Denied") {
t.Errorf("unexpected upload error: %v", err)
}
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
if err != nil {
t.Errorf("unexpected overwrite error: %v", err)
}
os.Remove(testFilePath)
}
httpd.RemoveUser(user, http.StatusOK)
os.RemoveAll(user.GetHomeDir())
}
func TestSubDirsDownloads(t *testing.T) {
usePubKey := true
u := getTestUser(usePubKey)
u.Permissions["/"] = []string{dataprovider.PermAny}
u.Permissions["/subdir"] = []string{dataprovider.PermChmod, dataprovider.PermUpload, dataprovider.PermListItems}
user, _, err := httpd.AddUser(u, http.StatusOK)
if err != nil {
t.Errorf("unable to add user: %v", err)
}
client, err := getSftpClient(user, usePubKey)
if err != nil {
t.Errorf("unable to create sftp client: %v", err)
} else {
defer client.Close()
err = client.Mkdir("subdir")
if err != nil {
t.Errorf("unexpected mkdir error: %v", err)
}
testFileName := "/subdir/test_file.dat"
testFilePath := filepath.Join(homeBasePath, "test_file.dat")
testFileSize := int64(65535)
err = createTestFile(testFilePath, testFileSize)
if err != nil {
t.Errorf("unable to create test file: %v", err)
}
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
if err != nil {
t.Errorf("file upload error: %v", err)
}
localDownloadPath := filepath.Join(homeBasePath, "test_download.dat")
err = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)
if !strings.Contains(err.Error(), "Permission Denied") {
t.Errorf("unexpected upload error: %v", err)
}
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
if !strings.Contains(err.Error(), "Permission Denied") {
t.Errorf("unexpected overwrite error: %v", err)
}
err = client.Chtimes(testFileName, time.Now(), time.Now())
if !strings.Contains(err.Error(), "Permission Denied") {
t.Errorf("unexpected chtimes error: %v", err)
}
err = client.Rename(testFileName, testFileName+".rename")
if !strings.Contains(err.Error(), "Permission Denied") {
t.Errorf("unexpected rename error: %v", err)
}
err = client.Symlink(testFileName, testFileName+".link")
if !strings.Contains(err.Error(), "Permission Denied") {
t.Errorf("unexpected symlink error: %v", err)
}
err = client.Remove(testFileName)
if !strings.Contains(err.Error(), "Permission Denied") {
t.Errorf("unexpected remove error: %v", err)
}
os.Remove(localDownloadPath)
os.Remove(testFilePath)
}
httpd.RemoveUser(user, http.StatusOK)
os.RemoveAll(user.GetHomeDir())
}
func TestPermsSubDirsSetstat(t *testing.T) {
// for setstat we check the parent dir permission if the requested path is a dir
// otherwise the path permission
usePubKey := true
u := getTestUser(usePubKey)
u.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermCreateDirs}
u.Permissions["/subdir"] = []string{dataprovider.PermAny}
user, _, err := httpd.AddUser(u, http.StatusOK)
if err != nil {
t.Errorf("unable to add user: %v", err)
}
client, err := getSftpClient(user, usePubKey)
if err != nil {
t.Errorf("unable to create sftp client: %v", err)
} else {
defer client.Close()
err = client.Mkdir("subdir")
if err != nil {
t.Errorf("unexpected mkdir error: %v", err)
}
testFileName := "/subdir/test_file.dat"
testFilePath := filepath.Join(homeBasePath, "test_file.dat")
testFileSize := int64(65535)
err = createTestFile(testFilePath, testFileSize)
if err != nil {
t.Errorf("unable to create test file: %v", err)
}
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
if err != nil {
t.Errorf("file upload error: %v", err)
}
err = client.Chtimes("/subdir/", time.Now(), time.Now())
if !strings.Contains(err.Error(), "Permission Denied") {
t.Errorf("unexpected chtimes error: %v", err)
}
err = client.Chtimes("subdir/", time.Now(), time.Now())
if !strings.Contains(err.Error(), "Permission Denied") {
t.Errorf("unexpected chtimes error: %v", err)
}
err = client.Chtimes(testFileName, time.Now(), time.Now())
if err != nil {
t.Errorf("unexpected chtimes error: %v", err)
}
os.Remove(testFilePath)
}
httpd.RemoveUser(user, http.StatusOK)
os.RemoveAll(user.GetHomeDir())
}
func TestPermsSubDirsCommands(t *testing.T) {
usePubKey := true
u := getTestUser(usePubKey)
u.Permissions["/"] = []string{dataprovider.PermAny}
u.Permissions["/subdir"] = []string{dataprovider.PermDownload, dataprovider.PermUpload}
user, _, err := httpd.AddUser(u, http.StatusOK)
if err != nil {
t.Errorf("unable to add user: %v", err)
}
client, err := getSftpClient(user, usePubKey)
if err != nil {
t.Errorf("unable to create sftp client: %v", err)
} else {
defer client.Close()
client.Mkdir("subdir")
acmodTime := time.Now()
err = client.Chtimes("/subdir", acmodTime, acmodTime)
if err != nil {
t.Errorf("unexpected chtimes error: %v", err)
}
_, err = client.Stat("/subdir")
if err != nil {
t.Errorf("unexpected stat error: %v", err)
}
_, err = client.ReadDir("/")
if err != nil {
t.Errorf("unexpected readdir error: %v", err)
}
_, err = client.ReadDir("/subdir")
if !strings.Contains(err.Error(), "Permission Denied") {
t.Errorf("unexpected error: %v", err)
}
err = client.RemoveDirectory("/subdir/dir")
if !strings.Contains(err.Error(), "Permission Denied") {
t.Errorf("unexpected error: %v", err)
}
err = client.Mkdir("/subdir/dir")
if !strings.Contains(err.Error(), "Permission Denied") {
t.Errorf("unexpected error: %v", err)
}
client.Mkdir("/otherdir")
err = client.Rename("/otherdir", "/subdir/otherdir")
if !strings.Contains(err.Error(), "Permission Denied") {
t.Errorf("unexpected error: %v", err)
}
err = client.Symlink("/otherdir", "/subdir/otherdir")
if !strings.Contains(err.Error(), "Permission Denied") {
t.Errorf("unexpected error: %v", err)
}
err = client.Symlink("/otherdir", "/otherdir_link")
if err != nil {
t.Errorf("unexpected rename dir error: %v", err)
}
err = client.Rename("/otherdir", "/otherdir1")
if err != nil {
t.Errorf("unexpected rename dir error: %v", err)
}
err = client.RemoveDirectory("/subdir")
if err != nil {
t.Errorf("unexpected remove dir error: %v", err)
}
}
httpd.RemoveUser(user, http.StatusOK)
os.RemoveAll(user.GetHomeDir())
}
func TestRelativePaths(t *testing.T) {
user := getTestUser(true)
path := filepath.Join(user.HomeDir, "/")
rel := user.GetRelativePath(path)
if rel != "/" {
t.Errorf("Unexpected relative path: %v", rel)
}
path = filepath.Join(user.HomeDir, "//")
rel = user.GetRelativePath(path)
if rel != "/" {
t.Errorf("Unexpected relative path: %v", rel)
}
path = filepath.Join(user.HomeDir, "../..")
rel = user.GetRelativePath(path)
if rel != "/" {
t.Errorf("Unexpected relative path: %v", rel)
}
path = filepath.Join(user.HomeDir, "../../../../../")
rel = user.GetRelativePath(path)
if rel != "/" {
t.Errorf("Unexpected relative path: %v", rel)
}
path = filepath.Join(user.HomeDir, "/..")
rel = user.GetRelativePath(path)
if rel != "/" {
t.Errorf("Unexpected relative path: %v", rel)
}
path = filepath.Join(user.HomeDir, "/../../../..")
rel = user.GetRelativePath(path)
if rel != "/" {
t.Errorf("Unexpected relative path: %v", rel)
}
path = filepath.Join(user.HomeDir, "")
rel = user.GetRelativePath(path)
if rel != "/" {
t.Errorf("Unexpected relative path: %v", rel)
}
path = filepath.Join(user.HomeDir, ".")
rel = user.GetRelativePath(path)
if rel != "/" {
t.Errorf("Unexpected relative path: %v", rel)
}
path = filepath.Join(user.HomeDir, "somedir")
rel = user.GetRelativePath(path)
if rel != "/somedir" {
t.Errorf("Unexpected relative path: %v", rel)
}
path = filepath.Join(user.HomeDir, "/somedir/subdir")
rel = user.GetRelativePath(path)
if rel != "/somedir/subdir" {
t.Errorf("Unexpected relative path: %v", rel)
}
}
func TestUserPerms(t *testing.T) {
user := getTestUser(true)
user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermListItems}
user.Permissions["/p"] = []string{dataprovider.PermDelete}
user.Permissions["/p/1"] = []string{dataprovider.PermDownload, dataprovider.PermUpload}
user.Permissions["/p/2"] = []string{dataprovider.PermCreateDirs}
user.Permissions["/p/3"] = []string{dataprovider.PermChmod}
user.Permissions["/p/3/4"] = []string{dataprovider.PermChtimes}
user.Permissions["/tmp"] = []string{dataprovider.PermRename}
if !user.HasPerm(dataprovider.PermListItems, filepath.Join(user.HomeDir, "/")) {
t.Error("expected permission not found")
}
if !user.HasPerm(dataprovider.PermListItems, filepath.Join(user.HomeDir, ".")) {
t.Error("expected permission not found")
}
if !user.HasPerm(dataprovider.PermListItems, filepath.Join(user.HomeDir, "")) {
t.Error("expected permission not found")
}
if !user.HasPerm(dataprovider.PermListItems, filepath.Join(user.HomeDir, "../")) {
t.Error("expected permission not found")
}
// path p and /p are the same
if !user.HasPerm(dataprovider.PermDelete, filepath.Join(user.HomeDir, "/p")) {
t.Error("expected permission not found")
}
if !user.HasPerm(dataprovider.PermDownload, filepath.Join(user.HomeDir, "/p/1")) {
t.Error("expected permission not found")
}
if !user.HasPerm(dataprovider.PermCreateDirs, filepath.Join(user.HomeDir, "p/2")) {
t.Error("expected permission not found")
}
if !user.HasPerm(dataprovider.PermChmod, filepath.Join(user.HomeDir, "/p/3")) {
t.Error("expected permission not found")
}
if !user.HasPerm(dataprovider.PermChtimes, filepath.Join(user.HomeDir, "p/3/4")) {
t.Error("expected permission not found")
}
if !user.HasPerm(dataprovider.PermChtimes, filepath.Join(user.HomeDir, "p/3/4/../4")) {
t.Error("expected permission not found")
}
// undefined paths have permissions of the nearest path
if !user.HasPerm(dataprovider.PermListItems, filepath.Join(user.HomeDir, "/p34")) {
t.Error("expected permission not found")
}
if !user.HasPerm(dataprovider.PermListItems, filepath.Join(user.HomeDir, "/p34/p1/file.dat")) {
t.Error("expected permission not found")
}
if !user.HasPerm(dataprovider.PermChtimes, filepath.Join(user.HomeDir, "/p/3/4/5/6")) {
t.Error("expected permission not found")
}
if !user.HasPerm(dataprovider.PermDownload, filepath.Join(user.HomeDir, "/p/1/test/file.dat")) {
t.Error("expected permission not found")
}
}
func TestSSHCommands(t *testing.T) { func TestSSHCommands(t *testing.T) {
usePubKey := false usePubKey := false
user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK)
@ -2208,6 +2599,21 @@ func TestSSHFileHash(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("file upload error: %v", err) t.Errorf("file upload error: %v", err)
} }
user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermUpload}
_, _, err = httpd.UpdateUser(user, http.StatusOK)
if err != nil {
t.Errorf("unable to update user: %v", err)
}
_, err = runSSHCommand("sha512sum "+testFileName, user, usePubKey)
if err == nil {
t.Errorf("hash command with no list permission must fail")
}
user.Permissions["/"] = []string{dataprovider.PermAny}
_, _, err = httpd.UpdateUser(user, http.StatusOK)
if err != nil {
t.Errorf("unable to update user: %v", err)
}
initialHash, err := computeHashForFile(sha512.New(), testFilePath) initialHash, err := computeHashForFile(sha512.New(), testFilePath)
if err != nil { if err != nil {
t.Errorf("error computing file hash: %v", err) t.Errorf("error computing file hash: %v", err)
@ -2523,13 +2929,57 @@ func TestSCPRecursive(t *testing.T) {
} }
} }
func TestSCPPermsSubDirs(t *testing.T) {
if len(scpPath) == 0 {
t.Skip("scp command not found, unable to execute this test")
}
usePubKey := true
u := getTestUser(usePubKey)
u.Permissions["/"] = []string{dataprovider.PermAny}
u.Permissions["/somedir"] = []string{dataprovider.PermListItems, dataprovider.PermUpload}
user, _, err := httpd.AddUser(u, http.StatusOK)
if err != nil {
t.Errorf("unable to add user: %v", err)
}
localPath := filepath.Join(homeBasePath, "scp_download.dat")
subPath := filepath.Join(user.GetHomeDir(), "somedir")
testFileSize := int64(65535)
os.MkdirAll(subPath, 0777)
remoteDownPath := fmt.Sprintf("%v@127.0.0.1:%v", user.Username, "/somedir")
err = scpDownload(localPath, remoteDownPath, false, true)
if err == nil {
t.Error("download a dir with no permissions must fail")
}
os.Remove(subPath)
err = createTestFile(subPath, testFileSize)
if err != nil {
t.Errorf("unable to create test file: %v", err)
}
err = scpDownload(localPath, remoteDownPath, false, false)
if err != nil {
t.Errorf("unexpected download error: %v", err)
}
os.Chmod(subPath, 0001)
err = scpDownload(localPath, remoteDownPath, false, false)
if err == nil {
t.Error("download a file with no system permissions must fail")
}
os.Chmod(subPath, 0755)
os.Remove(localPath)
os.RemoveAll(user.GetHomeDir())
_, err = httpd.RemoveUser(user, http.StatusOK)
if err != nil {
t.Errorf("unable to remove user: %v", err)
}
}
func TestSCPPermCreateDirs(t *testing.T) { func TestSCPPermCreateDirs(t *testing.T) {
if len(scpPath) == 0 { if len(scpPath) == 0 {
t.Skip("scp command not found, unable to execute this test") t.Skip("scp command not found, unable to execute this test")
} }
usePubKey := true usePubKey := true
u := getTestUser(usePubKey) u := getTestUser(usePubKey)
u.Permissions = []string{dataprovider.PermDownload, dataprovider.PermUpload} u.Permissions["/"] = []string{dataprovider.PermDownload, dataprovider.PermUpload}
user, _, err := httpd.AddUser(u, http.StatusOK) user, _, err := httpd.AddUser(u, http.StatusOK)
if err != nil { if err != nil {
t.Errorf("unable to add user: %v", err) t.Errorf("unable to add user: %v", err)
@ -2551,7 +3001,7 @@ func TestSCPPermCreateDirs(t *testing.T) {
remoteUpPath := fmt.Sprintf("%v@127.0.0.1:%v", user.Username, "/tmp/") remoteUpPath := fmt.Sprintf("%v@127.0.0.1:%v", user.Username, "/tmp/")
err = scpUpload(testFilePath, remoteUpPath, true, false) err = scpUpload(testFilePath, remoteUpPath, true, false)
if err == nil { if err == nil {
t.Errorf("scp upload must fail, the user cannot create new dirs") t.Errorf("scp upload must fail, the user cannot create files in a missing dir")
} }
err = scpUpload(testBaseDirPath, remoteUpPath, true, false) err = scpUpload(testBaseDirPath, remoteUpPath, true, false)
if err == nil { if err == nil {
@ -2578,7 +3028,7 @@ func TestSCPPermUpload(t *testing.T) {
} }
usePubKey := true usePubKey := true
u := getTestUser(usePubKey) u := getTestUser(usePubKey)
u.Permissions = []string{dataprovider.PermDownload, dataprovider.PermCreateDirs} u.Permissions["/"] = []string{dataprovider.PermDownload, dataprovider.PermCreateDirs}
user, _, err := httpd.AddUser(u, http.StatusOK) user, _, err := httpd.AddUser(u, http.StatusOK)
if err != nil { if err != nil {
t.Errorf("unable to add user: %v", err) t.Errorf("unable to add user: %v", err)
@ -2615,7 +3065,7 @@ func TestSCPPermOverwrite(t *testing.T) {
} }
usePubKey := true usePubKey := true
u := getTestUser(usePubKey) u := getTestUser(usePubKey)
u.Permissions = []string{dataprovider.PermUpload, dataprovider.PermCreateDirs} u.Permissions["/"] = []string{dataprovider.PermUpload, dataprovider.PermCreateDirs}
user, _, err := httpd.AddUser(u, http.StatusOK) user, _, err := httpd.AddUser(u, http.StatusOK)
if err != nil { if err != nil {
t.Errorf("unable to add user: %v", err) t.Errorf("unable to add user: %v", err)
@ -2656,7 +3106,7 @@ func TestSCPPermDownload(t *testing.T) {
} }
usePubKey := true usePubKey := true
u := getTestUser(usePubKey) u := getTestUser(usePubKey)
u.Permissions = []string{dataprovider.PermUpload, dataprovider.PermCreateDirs} u.Permissions["/"] = []string{dataprovider.PermUpload, dataprovider.PermCreateDirs}
user, _, err := httpd.AddUser(u, http.StatusOK) user, _, err := httpd.AddUser(u, http.StatusOK)
if err != nil { if err != nil {
t.Errorf("unable to add user: %v", err) t.Errorf("unable to add user: %v", err)
@ -2668,12 +3118,12 @@ func TestSCPPermDownload(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("unable to create test file: %v", err) t.Errorf("unable to create test file: %v", err)
} }
remoteUpPath := fmt.Sprintf("%v@127.0.0.1:%v", user.Username, "tmp") remoteUpPath := fmt.Sprintf("%v@127.0.0.1:%v", user.Username, "/")
err = scpUpload(testFilePath, remoteUpPath, true, false) err = scpUpload(testFilePath, remoteUpPath, true, false)
if err != nil { if err != nil {
t.Errorf("error uploading existing file via scp: %v", err) t.Errorf("error uploading existing file via scp: %v", err)
} }
remoteDownPath := fmt.Sprintf("%v@127.0.0.1:%v", user.Username, path.Join("/tmp", testFileName)) remoteDownPath := fmt.Sprintf("%v@127.0.0.1:%v", user.Username, path.Join("/", testFileName))
localPath := filepath.Join(homeBasePath, "scp_download.dat") localPath := filepath.Join(homeBasePath, "scp_download.dat")
err = scpDownload(localPath, remoteDownPath, false, false) err = scpDownload(localPath, remoteDownPath, false, false)
if err == nil { if err == nil {
@ -3008,10 +3458,11 @@ func getTestUser(usePubKey bool) dataprovider.User {
Username: defaultUsername, Username: defaultUsername,
Password: defaultPassword, Password: defaultPassword,
HomeDir: filepath.Join(homeBasePath, defaultUsername), HomeDir: filepath.Join(homeBasePath, defaultUsername),
Permissions: allPerms,
Status: 1, Status: 1,
ExpirationDate: 0, ExpirationDate: 0,
} }
user.Permissions = make(map[string][]string)
user.Permissions["/"] = allPerms
if usePubKey { if usePubKey {
user.PublicKeys = []string{testPubKey} user.PublicKeys = []string{testPubKey}
user.Password = "" user.Password = ""
@ -3134,10 +3585,10 @@ func sftpUploadFile(localSourcePath string, remoteDestPath string, expectedSize
return err return err
} }
// we need to close the file to trigger the close method on server // we need to close the file to trigger the close method on server
// we cannot defer closing or Lstat will fail for upload atomic mode // we cannot defer closing or Lstat will fail for uploads in atomic mode
destFile.Close() destFile.Close()
if expectedSize > 0 { if expectedSize > 0 {
fi, err := client.Lstat(remoteDestPath) fi, err := client.Stat(remoteDestPath)
if err != nil { if err != nil {
return err return err
} }

View file

@ -129,6 +129,9 @@ func (c *sshCommand) handleHashCommands() error {
if err != nil { if err != nil {
return c.sendErrorResponse(err) return c.sendErrorResponse(err)
} }
if !c.connection.User.HasPerm(dataprovider.PermListItems, path) {
return c.sendErrorResponse(errPermissionDenied)
}
hash, err := computeHashForFile(h, path) hash, err := computeHashForFile(h, path)
if err != nil { if err != nil {
return c.sendErrorResponse(err) return c.sendErrorResponse(err)
@ -146,7 +149,7 @@ func (c *sshCommand) executeSystemCommand(command systemCommand) error {
} }
perms := []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermCreateDirs, dataprovider.PermListItems, perms := []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermCreateDirs, dataprovider.PermListItems,
dataprovider.PermOverwrite, dataprovider.PermDelete, dataprovider.PermRename} dataprovider.PermOverwrite, dataprovider.PermDelete, dataprovider.PermRename}
if !c.connection.User.HasPerms(perms) { if !c.connection.User.HasPerms(perms, command.realPath) {
return c.sendErrorResponse(errPermissionDenied) return c.sendErrorResponse(errPermissionDenied)
} }
@ -277,23 +280,6 @@ func (c *sshCommand) getSystemCommand() (systemCommand, error) {
} }
args := make([]string, len(c.args)) args := make([]string, len(c.args))
copy(args, c.args) copy(args, c.args)
if c.command == "rsync" {
// we cannot avoid that rsync create symlinks so if the user has the permission
// to create symlinks we add the option --safe-links to the received rsync command if
// it is not already set. This should prevent to create symlinks that point outside
// the home dir.
// If the user cannot create symlinks we add the option --munge-links, if it is not
// already set. This should make symlinks unusable (but manually recoverable)
if c.connection.User.HasPerm(dataprovider.PermCreateSymlinks) {
if !utils.IsStringInSlice("--safe-links", args) {
args = append([]string{"--safe-links"}, args...)
}
} else {
if !utils.IsStringInSlice("--munge-links", args) {
args = append([]string{"--munge-links"}, args...)
}
}
}
var path string var path string
if len(c.args) > 0 { if len(c.args) > 0 {
var err error var err error
@ -305,6 +291,23 @@ func (c *sshCommand) getSystemCommand() (systemCommand, error) {
args = args[:len(args)-1] args = args[:len(args)-1]
args = append(args, path) args = append(args, path)
} }
if c.command == "rsync" {
// we cannot avoid that rsync create symlinks so if the user has the permission
// to create symlinks we add the option --safe-links to the received rsync command if
// it is not already set. This should prevent to create symlinks that point outside
// the home dir.
// If the user cannot create symlinks we add the option --munge-links, if it is not
// already set. This should make symlinks unusable (but manually recoverable)
if c.connection.User.HasPerm(dataprovider.PermCreateSymlinks, path) {
if !utils.IsStringInSlice("--safe-links", args) {
args = append([]string{"--safe-links"}, args...)
}
} else {
if !utils.IsStringInSlice("--munge-links", args) {
args = append([]string{"--munge-links"}, args...)
}
}
}
c.connection.Log(logger.LevelDebug, logSenderSSH, "new system command %#v, with args: %v path: %v", c.command, args, path) c.connection.Log(logger.LevelDebug, logSenderSSH, "new system command %#v, with args: %v path: %v", c.command, args, path)
cmd := exec.Command(c.command, args...) cmd := exec.Command(c.command, args...)
uid := c.connection.User.GetUID() uid := c.connection.User.GetUID()

View file

@ -76,13 +76,28 @@
<select class="form-control" id="idPermissions" name="permissions" required multiple> <select class="form-control" id="idPermissions" name="permissions" required multiple>
{{range $validPerm := .ValidPerms}} {{range $validPerm := .ValidPerms}}
<option value="{{$validPerm}}" <option value="{{$validPerm}}"
{{range $perm := $.User.Permissions}}{{if eq $perm $validPerm}}selected{{end}}{{end}}>{{$validPerm}} {{range $perm := $.RootDirPerms }}{{if eq $perm $validPerm}}selected{{end}}{{end}}>{{$validPerm}}
</option> </option>
{{end}} {{end}}
</select> </select>
</div> </div>
</div> </div>
<div class="form-group row">
<label for="idSubDirsPermissions" class="col-sm-2 col-form-label">Sub dirs permissions</label>
<div class="col-sm-10">
<textarea class="form-control" id="idSubDirsPermissions" name="sub_dirs_permissions" rows="3"
aria-describedby="subDirsHelpBlock">{{range $dir, $perms := .User.Permissions -}}
{{if ne $dir "/" -}}
{{$dir}}:{{range $index, $p := $perms}}{{if $index}},{{end}}{{$p}}{{end}}&#10;
{{- end}}
{{- end}}</textarea>
<small id="subDirsHelpBlock" class="form-text text-muted">
One directory per line as dir:perms, for example /somedir:list,download
</small>
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<label for="idHomeDir" class="col-sm-2 col-form-label">Home Dir</label> <label for="idHomeDir" class="col-sm-2 col-form-label">Home Dir</label>
<div class="col-sm-10"> <div class="col-sm-10">