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.
- Bandwidth throttling is supported, with distinct settings for upload and download.
- 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).
- 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.
@ -367,7 +367,7 @@ For each account the following properties can be configured:
- `max_sessions` maximum concurrent sessions. 0 means unlimited.
- `quota_size` maximum size allowed as bytes. 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
- `list` list items 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) {
portableDir, _ = filepath.Abs(portableDir)
}
permissions := make(map[string][]string)
permissions["/"] = portablePermissions
service := service.Service{
ConfigDir: defaultConfigDir,
ConfigFile: defaultConfigName,
@ -48,7 +50,7 @@ Please take a look at the usage below to customize the serving parameters`,
Username: portableUsername,
Password: portablePassword,
PublicKeys: portablePublicKeys,
Permissions: portablePermissions,
Permissions: permissions,
HomeDir: portableDir,
Status: 1,
},

View file

@ -14,7 +14,7 @@ import (
)
const (
databaseVersion = 2
databaseVersion = 3
)
var (
@ -33,6 +33,28 @@ type boltDatabaseVersion struct {
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 {
var err error
logSender = BoltDataProviderName
@ -376,27 +398,91 @@ func checkBoltDatabaseVersion(dbHandle *bolt.DB) error {
return nil
}
if dbVersion.Version == 1 {
providerLog(logger.LevelInfo, "update bolt database version: 1 -> 2")
usernames, err := getBoltAvailableUsernames(dbHandle)
err = updateDatabaseFrom1To2(dbHandle)
if err != nil {
return err
}
for _, u := range usernames {
user, err := provider.userExists(u)
if err != nil {
return err
}
user.Status = 1
err = provider.updateUser(user)
if err != nil {
return err
}
providerLog(logger.LevelInfo, "user %#v updated, \"status\" setted to 1", user.Username)
}
return updateBoltDatabaseVersion(dbHandle, 2)
return updateDatabaseFrom2To3(dbHandle)
} else if dbVersion.Version == 2 {
return updateDatabaseFrom2To3(dbHandle)
}
return err
return nil
}
func updateDatabaseFrom1To2(dbHandle *bolt.DB) error {
providerLog(logger.LevelInfo, "updating bolt database version: 1 -> 2")
usernames, err := getBoltAvailableUsernames(dbHandle)
if err != nil {
return err
}
for _, u := range usernames {
user, err := provider.userExists(u)
if err != nil {
return err
}
user.Status = 1
err = provider.updateUser(user)
if err != nil {
return err
}
providerLog(logger.LevelInfo, "user %#v updated, \"status\" setted to 1", user.Username)
}
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
}
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) {

View file

@ -18,6 +18,7 @@ import (
"net/url"
"os"
"os/exec"
"path"
"path/filepath"
"strconv"
"strings"
@ -343,14 +344,33 @@ func buildUserHomeDir(user *User) {
}
func validatePermissions(user *User) error {
for _, p := range user.Permissions {
if !utils.IsStringInSlice(p, ValidPerms) {
return &ValidationError{err: fmt.Sprintf("Invalid permission: %v", p)}
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) {
return &ValidationError{err: fmt.Sprintf("Invalid permission: %#v", p)}
}
}
cleanedDir := filepath.ToSlash(path.Clean(dir))
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
}
}
if utils.IsStringInSlice(PermAny, user.Permissions) {
user.Permissions = []string{PermAny}
}
user.Permissions = permissions
return nil
}

View file

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

View file

@ -3,8 +3,10 @@ package dataprovider
import (
"encoding/json"
"fmt"
"path"
"path/filepath"
"strconv"
"strings"
"github.com/drakkan/sftpgo/utils"
)
@ -68,7 +70,7 @@ type User struct {
// Maximum number of files allowed. 0 means unlimited
QuotaFiles int `json:"quota_files"`
// List of the granted permissions
Permissions []string `json:"permissions"`
Permissions map[string][]string `json:"permissions"`
// Used quota as bytes
UsedQuotaSize int64 `json:"used_quota_size"`
// Used quota as number of files
@ -83,21 +85,59 @@ type User struct {
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
func (u *User) HasPerm(permission string) bool {
if utils.IsStringInSlice(PermAny, u.Permissions) {
func (u *User) HasPerm(permission, path string) bool {
perms := u.GetPermissionsForPath(path)
if utils.IsStringInSlice(PermAny, perms) {
return true
}
return utils.IsStringInSlice(permission, u.Permissions)
return utils.IsStringInSlice(permission, perms)
}
// HasPerms return true if the user has all the given permissions
func (u *User) HasPerms(permissions []string) bool {
if utils.IsStringInSlice(PermAny, u.Permissions) {
func (u *User) HasPerms(permissions []string, path string) bool {
perms := u.GetPermissionsForPath(path)
if utils.IsStringInSlice(PermAny, perms) {
return true
}
for _, permission := range permissions {
if !utils.IsStringInSlice(permission, u.Permissions) {
if !utils.IsStringInSlice(permission, perms) {
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.
// This is the path as seen by SFTP users
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 {
return ""
}
if rel == "." || strings.HasPrefix(rel, "..") {
rel = ""
}
return "/" + filepath.ToSlash(rel)
}
@ -168,12 +211,28 @@ func (u *User) GetQuotaSummary() string {
// GetPermissionsAsString returns the user's permissions as comma separated string
func (u *User) GetPermissionsAsString() string {
var result string
for _, p := range u.Permissions {
if len(result) > 0 {
result += ", "
result := ""
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 {
result += ", "
}
result += dp
}
result += p
}
return result
}
@ -230,8 +289,12 @@ func (u *User) GetExpirationDateAsString() string {
func (u *User) getACopy() User {
pubKeys := make([]string, len(u.PublicKeys))
copy(pubKeys, u.PublicKeys)
permissions := make([]string, len(u.Permissions))
copy(permissions, u.Permissions)
permissions := make(map[string][]string)
for k, v := range u.Permissions {
perms := make([]string, len(v))
copy(perms, v)
permissions[k] = perms
}
return User{
ID: u.ID,
Username: u.Username,

View file

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

View file

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

View file

@ -194,20 +194,31 @@ func TestAddUserInvalidHomeDir(t *testing.T) {
func TestAddUserNoPerms(t *testing.T) {
u := getTestUser()
u.Permissions = []string{}
u.Permissions = make(map[string][]string)
_, _, err := httpd.AddUser(u, http.StatusBadRequest)
if err != nil {
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) {
u := getTestUser()
u.Permissions = []string{"invalidPerm"}
u.Permissions["/"] = []string{"invalidPerm"}
_, _, err := httpd.AddUser(u, http.StatusBadRequest)
if err != nil {
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) {
@ -251,7 +262,8 @@ func TestUpdateUser(t *testing.T) {
user.MaxSessions = 10
user.QuotaSize = 4096
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.DownloadBandwidth = 512
user, _, err = httpd.UpdateUser(user, http.StatusOK)
@ -556,7 +568,7 @@ func TestBasicUserHandlingMock(t *testing.T) {
checkResponseCode(t, http.StatusInternalServerError, rr.Code)
user.MaxSessions = 10
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)
req, _ = http.NewRequest(http.MethodPut, userPath+"/"+strconv.FormatInt(user.ID, 10), bytes.NewBuffer(userAsJSON))
rr = executeRequest(req)
@ -574,10 +586,10 @@ func TestBasicUserHandlingMock(t *testing.T) {
if user.MaxSessions != updatedUser.MaxSessions || user.UploadBandwidth != 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")
}
if !utils.IsStringInSlice(dataprovider.PermAny, updatedUser.Permissions) {
if !utils.IsStringInSlice(dataprovider.PermAny, updatedUser.Permissions["/"]) {
t.Errorf("permissions mismatch")
}
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) {
user := getTestUser()
user.Permissions = []string{}
user.Permissions["/"] = []string{}
userAsJSON := getUserAsJSON(t, user)
req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))
rr := executeRequest(req)
@ -627,6 +639,112 @@ func TestAddUserInvalidJsonMock(t *testing.T) {
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) {
user := getTestUser()
userAsJSON := getUserAsJSON(t, user)
@ -924,6 +1042,7 @@ func TestWebUserAddMock(t *testing.T) {
form.Set("status", strconv.Itoa(user.Status))
form.Set("expiration_date", "")
form.Set("permissions", "*")
form.Set("sub_dirs_permissions", "/subdir:list,download")
// test invalid url escape
req, _ := http.NewRequest(http.MethodPost, webUserPath+"?a=%2", strings.NewReader(form.Encode()))
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("download_bandwidth", "0")
form.Set("permissions", "*")
form.Set("sub_dirs_permissions", "/otherdir:list,upload")
form.Set("status", strconv.Itoa(user.Status))
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()))
@ -1142,13 +1262,15 @@ func waitTCPListening(address string) {
}
func getTestUser() dataprovider.User {
return dataprovider.User{
Username: defaultUsername,
Password: defaultPassword,
HomeDir: filepath.Join(homeBasePath, defaultUsername),
Permissions: defaultPerms,
Status: 1,
user := dataprovider.User{
Username: defaultUsername,
Password: defaultPassword,
HomeDir: filepath.Join(homeBasePath, defaultUsername),
Status: 1,
}
user.Permissions = make(map[string][]string)
user.Permissions["/"] = defaultPerms
return user
}
func getUserAsJSON(t *testing.T, user dataprovider.User) []byte {

View file

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

View file

@ -2,7 +2,7 @@ openapi: 3.0.1
info:
title: SFTPGo
description: 'SFTPGo REST API'
version: 1.2.0
version: 1.3.0
servers:
- url: /api/v1
@ -560,6 +560,15 @@ components:
* `chmod` changing file or directory permissions is allowed
* `chown` changing file or directory owner and group 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:
type: object
properties:
@ -620,10 +629,11 @@ components:
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
permissions:
type: array
type: object
items:
$ref: '#/components/schemas/Permission'
$ref: '#/components/schemas/DirPermissions'
minItems: 1
example: {"/":["*"],"/somedir":["list","download"]}
used_quota_size:
type: integer
format: int64

View file

@ -61,10 +61,12 @@ type connectionsPage struct {
type userPage struct {
basePage
IsAdd bool
User dataprovider.User
Error string
ValidPerms []string
IsAdd bool
User dataprovider.User
RootPerms []string
Error string
ValidPerms []string
RootDirPerms []string
}
type messagePage struct {
@ -156,26 +158,54 @@ func renderNotFoundPage(w http.ResponseWriter, err error) {
func renderAddUserPage(w http.ResponseWriter, user dataprovider.User, error string) {
data := userPage{
basePage: getBasePageData("Add a new user", webUserPath),
IsAdd: true,
Error: error,
User: user,
ValidPerms: dataprovider.ValidPerms,
basePage: getBasePageData("Add a new user", webUserPath),
IsAdd: true,
Error: error,
User: user,
ValidPerms: dataprovider.ValidPerms,
RootDirPerms: user.GetPermissionsForPath("/"),
}
renderTemplate(w, templateUser, data)
}
func renderUpdateUserPage(w http.ResponseWriter, user dataprovider.User, error string) {
data := userPage{
basePage: getBasePageData("Update user", fmt.Sprintf("%v/%v", webUserPath, user.ID)),
IsAdd: false,
Error: error,
User: user,
ValidPerms: dataprovider.ValidPerms,
basePage: getBasePageData("Update user", fmt.Sprintf("%v/%v", webUserPath, user.ID)),
IsAdd: false,
Error: error,
User: user,
ValidPerms: dataprovider.ValidPerms,
RootDirPerms: user.GetPermissionsForPath("/"),
}
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) {
var user dataprovider.User
err := r.ParseForm()
@ -238,7 +268,7 @@ func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
HomeDir: r.Form.Get("home_dir"),
UID: uid,
GID: gid,
Permissions: r.Form["permissions"],
Permissions: getUserPermissionsFromPostFields(r),
MaxSessions: maxSessions,
QuotaSize: quotaSize,
QuotaFiles: quotaFiles,

View file

@ -41,38 +41,47 @@ Let's see a sample usage for each REST API.
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:
```json
{
"id": 5140,
"username": "test_username",
"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"
],
"used_quota_size": 0,
"used_quota_files": 0,
"last_quota_update": 0,
"last_login": 0,
"download_bandwidth": 60,
"expiration_date": 1546297200000,
"gid": 1000,
"home_dir": "/tmp/test_home_dir",
"id": 9576,
"last_login": 0,
"last_quota_update": 0,
"max_sessions": 2,
"permissions": {
"/": [
"list",
"download",
"upload",
"delete",
"rename",
"create_dirs",
"overwrite"
],
"/dir1": [
"list",
"download"
],
"/dir2": [
"*"
]
},
"quota_files": 3,
"quota_size": 0,
"status": 0,
"uid": 33,
"upload_bandwidth": 100,
"download_bandwidth": 60
"used_quota_files": 0,
"used_quota_size": 0,
"username": "test_username"
}
```
@ -81,7 +90,7 @@ Output:
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:
@ -99,32 +108,39 @@ Output:
Command:
```
python sftpgo_api_cli.py get-user-by-id 5140
python sftpgo_api_cli.py get-user-by-id 9576
```
Output:
```json
{
"id": 5140,
"username": "test_username",
"home_dir": "/tmp/test_home_dir",
"uid": 0,
"gid": 33,
"max_sessions": 2,
"quota_size": 0,
"quota_files": 4,
"permissions": [
"*"
],
"used_quota_size": 0,
"used_quota_files": 0,
"last_quota_update": 0,
"last_login": 0,
"download_bandwidth": 80,
"expiration_date": 0,
"gid": 33,
"home_dir": "/tmp/test_home_dir",
"id": 9576,
"last_login": 0,
"last_quota_update": 0,
"max_sessions": 3,
"permissions": {
"/": [
"*"
],
"/dir1": [
"list",
"download",
"create_symlinks"
]
},
"quota_files": 4,
"quota_size": 0,
"status": 1,
"uid": 0,
"upload_bandwidth": 90,
"download_bandwidth": 80
"used_quota_files": 0,
"used_quota_size": 0,
"username": "test_username"
}
```
@ -141,25 +157,32 @@ Output:
```json
[
{
"id": 5140,
"username": "test_username",
"home_dir": "/tmp/test_home_dir",
"uid": 0,
"gid": 33,
"max_sessions": 2,
"quota_size": 0,
"quota_files": 4,
"permissions": [
"*"
],
"used_quota_size": 0,
"used_quota_files": 0,
"last_quota_update": 0,
"last_login": 0,
"download_bandwidth": 80,
"expiration_date": 0,
"gid": 33,
"home_dir": "/tmp/test_home_dir",
"id": 9576,
"last_login": 0,
"last_quota_update": 0,
"max_sessions": 3,
"permissions": {
"/": [
"*"
],
"/dir1": [
"list",
"download",
"create_symlinks"
]
},
"quota_files": 4,
"quota_size": 0,
"status": 1,
"uid": 0,
"upload_bandwidth": 90,
"download_bandwidth": 80
"used_quota_files": 0,
"used_quota_size": 0,
"username": "test_username"
}
]
```
@ -177,23 +200,23 @@ Output:
```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": [
{
"last_activity": 1577197485561,
"operation_type": "upload",
"path": "/test_upload.gz",
"start_time": 1564696149783,
"size": 1146880,
"last_activity": 1564696159605
"path": "/test_upload.tar.gz",
"size": 1540096,
"start_time": 1577197471372
}
]
],
"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:
```
python sftpgo_api_cli.py close-connection 76a11b22260ee4249328df28bef34dc64c70f7c097db52159fc24049eeb0e32c
python sftpgo_api_cli.py close-connection f82cfec6a391ad673edd4ae9a144f32ccb59456139f8e1185b070134fffbab7c
```
Output:
@ -247,7 +270,7 @@ Output:
Command:
```
python sftpgo_api_cli.py delete-user 5140
python sftpgo_api_cli.py delete-user 9576
```
Output:
@ -272,9 +295,9 @@ Output:
```json
{
"version": "0.9.0-dev",
"build_date": "2019-08-08T08:11:34Z",
"commit_hash": "4f4489d-dirty"
"build_date": "2019-12-24T14:17:47Z",
"commit_hash": "f8fd5c0-dirty",
"version": "0.9.4-dev"
}
```

View file

@ -1,8 +1,8 @@
#!/usr/bin/env python
from datetime import datetime
import argparse
from datetime import datetime
import json
import requests
try:
@ -60,7 +60,7 @@ class SFTPGoApiRequests:
print(r.text)
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):
user = {"id":user_id, "username":username, "uid":uid, "gid":gid,
"max_sessions":max_sessions, "quota_size":quota_size, "quota_files":quota_files,
@ -76,6 +76,23 @@ class SFTPGoApiRequests:
user.update({"permissions":permissions})
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=""):
r = requests.get(self.userPath, params={"limit":limit, "offset":offset, "order":order,
"username":username}, auth=self.auth, verify=self.verify)
@ -86,18 +103,20 @@ class SFTPGoApiRequests:
self.printResponse(r)
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,
expiration_date=0):
quota_size=0, quota_files=0, perms=[], upload_bandwidth=0, download_bandwidth=0, status=1,
expiration_date=0, subdirs_permissions=[]):
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)
self.printResponse(r)
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,
download_bandwidth=0, status=1, expiration_date=0):
max_sessions=0, quota_size=0, quota_files=0, perms=[], upload_bandwidth=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,
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)
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('-G', '--permissions', type=str, nargs='+', default=[],
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,
help='Maximum upload bandwidth as KB/s, 0 means unlimited. Default: %(default)s')
parser.add_argument('-D', '--download-bandwidth', type=int, default=0,
@ -237,11 +259,12 @@ if __name__ == '__main__':
if args.command == 'add-user':
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.status, getDatetimeAsMillisSinceEpoch(args.expiration_date))
args.status, getDatetimeAsMillisSinceEpoch(args.expiration_date), args.subdirs_permissions)
elif args.command == 'update-user':
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.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':
api.deleteUser(args.id)
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) {
updateConnectionActivity(c.ID)
if !c.User.HasPerm(dataprovider.PermDownload) {
return nil, sftp.ErrSSHFxPermissionDenied
}
p, err := c.buildPath(request.Filepath)
if err != nil {
return nil, getSFTPErrorFromOSError(err)
}
if !c.User.HasPerm(dataprovider.PermDownload, filepath.Dir(p)) {
return nil, sftp.ErrSSHFxPermissionDenied
}
c.lock.Lock()
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.
func (c Connection) Filewrite(request *sftp.Request) (io.WriterAt, error) {
updateConnectionActivity(c.ID)
if !c.User.HasPerm(dataprovider.PermUpload) {
return nil, sftp.ErrSSHFxPermissionDenied
}
p, err := c.buildPath(request.Filepath)
if err != nil {
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
// leading up to where that file will be created.
if os.IsNotExist(statErr) {
if !c.User.HasPerm(dataprovider.PermUpload, filepath.Dir(p)) {
return nil, sftp.ErrSSHFxPermissionDenied
}
return c.handleSFTPUploadToNewFile(p, filePath)
}
@ -133,7 +131,7 @@ func (c Connection) Filewrite(request *sftp.Request) (io.WriterAt, error) {
return nil, sftp.ErrSSHFxOpUnsupported
}
if !c.User.HasPerm(dataprovider.PermOverwrite) {
if !c.User.HasPerm(dataprovider.PermOverwrite, filepath.Dir(filePath)) {
return nil, sftp.ErrSSHFxPermissionDenied
}
@ -212,7 +210,7 @@ func (c Connection) Filelist(request *sftp.Request) (sftp.ListerAt, error) {
switch request.Method {
case "List":
if !c.User.HasPerm(dataprovider.PermListItems) {
if !c.User.HasPerm(dataprovider.PermListItems, p) {
return nil, sftp.ErrSSHFxPermissionDenied
}
@ -226,7 +224,7 @@ func (c Connection) Filelist(request *sftp.Request) (sftp.ListerAt, error) {
return listerAt(files), nil
case "Stat":
if !c.User.HasPerm(dataprovider.PermListItems) {
if !c.User.HasPerm(dataprovider.PermListItems, filepath.Dir(p)) {
return nil, sftp.ErrSSHFxPermissionDenied
}
@ -266,9 +264,15 @@ func (c Connection) handleSFTPSetstat(path string, request *sftp.Request) error
c.ClientVersion)
return sftp.ErrSSHFxBadMessage
}
pathForPerms := path
if fi, err := os.Lstat(path); err == nil {
if fi.IsDir() {
pathForPerms = filepath.Dir(path)
}
}
attrFlags := request.AttrFlags()
if attrFlags.Permissions {
if !c.User.HasPerm(dataprovider.PermChmod) {
if !c.User.HasPerm(dataprovider.PermChmod, pathForPerms) {
return sftp.ErrSSHFxPermissionDenied
}
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, "", "", "")
return nil
} else if attrFlags.UidGid {
if !c.User.HasPerm(dataprovider.PermChown) {
if !c.User.HasPerm(dataprovider.PermChown, pathForPerms) {
return sftp.ErrSSHFxPermissionDenied
}
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, "", "", "")
return nil
} else if attrFlags.Acmodtime {
if !c.User.HasPerm(dataprovider.PermChtimes) {
if !c.User.HasPerm(dataprovider.PermChtimes, pathForPerms) {
return sftp.ErrSSHFxPermissionDenied
}
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 {
if !c.User.HasPerm(dataprovider.PermRename) {
if !c.User.HasPerm(dataprovider.PermRename, filepath.Dir(targetPath)) {
return sftp.ErrSSHFxPermissionDenied
}
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 {
if !c.User.HasPerm(dataprovider.PermDelete) {
if !c.User.HasPerm(dataprovider.PermDelete, filepath.Dir(path)) {
return sftp.ErrSSHFxPermissionDenied
}
@ -350,7 +354,7 @@ func (c Connection) handleSFTPRmdir(path 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
}
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 {
if !c.User.HasPerm(dataprovider.PermCreateDirs) {
if !c.User.HasPerm(dataprovider.PermCreateDirs, filepath.Dir(path)) {
return sftp.ErrSSHFxPermissionDenied
}
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 {
if !c.User.HasPerm(dataprovider.PermDelete) {
if !c.User.HasPerm(dataprovider.PermDelete, filepath.Dir(path)) {
return sftp.ErrSSHFxPermissionDenied
}

View file

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

View file

@ -114,19 +114,18 @@ func (c *scpCommand) handleRecursiveUpload() error {
func (c *scpCommand) handleCreateDir(dirPath string) error {
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)
if err != nil {
c.connection.Log(logger.LevelWarn, logSenderSCP, "error creating dir: %#v, invalid file path, err: %v", dirPath, err)
c.sendErrorMessage(err.Error())
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)
if err != nil {
@ -188,15 +187,6 @@ func (c *scpCommand) handleUploadFile(requestPath, filePath string, sizeToRead i
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)
if err != nil {
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
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)
if err != nil {
@ -250,6 +234,12 @@ func (c *scpCommand) handleUpload(uploadFilePath string, sizeToRead int64) error
}
stat, statErr := os.Stat(p)
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)
}
@ -266,7 +256,7 @@ func (c *scpCommand) handleUpload(uploadFilePath string, sizeToRead int64) error
return err
}
if !c.connection.User.HasPerm(dataprovider.PermOverwrite) {
if !c.connection.User.HasPerm(dataprovider.PermOverwrite, filePath) {
err := fmt.Errorf("Permission denied")
c.connection.Log(logger.LevelWarn, logSenderSCP, "cannot overwrite file: %#v, permission denied", uploadFilePath)
c.sendErrorMessage(err.Error())
@ -425,12 +415,6 @@ func (c *scpCommand) handleDownload(filePath string) error {
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)
if err != nil {
err := fmt.Errorf("Invalid file path")
@ -447,10 +431,23 @@ func (c *scpCommand) handleDownload(filePath string) error {
}
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)
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)
if err != nil {
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))
exitCode := m.Run()
//os.Remove(logfilePath)
os.Remove(logfilePath)
os.Remove(loginBannerFile)
os.Remove(pubKeyPath)
os.Remove(privateKeyPath)
@ -1395,6 +1395,7 @@ func TestMissingFile(t *testing.T) {
if err == nil {
t.Errorf("download missing file must fail")
}
os.Remove(localDownloadPath)
}
_, err = httpd.RemoveUser(user, http.StatusOK)
if err != nil {
@ -1697,7 +1698,7 @@ func TestPasswordsHashSHA512Crypt(t *testing.T) {
func TestPermList(t *testing.T) {
usePubKey := true
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.PermChown, dataprovider.PermChtimes}
user, _, err := httpd.AddUser(u, http.StatusOK)
@ -1728,7 +1729,7 @@ func TestPermList(t *testing.T) {
func TestPermDownload(t *testing.T) {
usePubKey := true
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.PermChown, dataprovider.PermChtimes}
user, _, err := httpd.AddUser(u, http.StatusOK)
@ -1773,7 +1774,7 @@ func TestPermDownload(t *testing.T) {
func TestPermUpload(t *testing.T) {
usePubKey := false
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.PermChown, dataprovider.PermChtimes}
user, _, err := httpd.AddUser(u, http.StatusOK)
@ -1808,7 +1809,7 @@ func TestPermUpload(t *testing.T) {
func TestPermOverwrite(t *testing.T) {
usePubKey := false
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.PermChown, dataprovider.PermChtimes}
user, _, err := httpd.AddUser(u, http.StatusOK)
@ -1847,7 +1848,7 @@ func TestPermOverwrite(t *testing.T) {
func TestPermDelete(t *testing.T) {
usePubKey := false
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.PermChown, dataprovider.PermChtimes}
user, _, err := httpd.AddUser(u, http.StatusOK)
@ -1886,7 +1887,7 @@ func TestPermDelete(t *testing.T) {
func TestPermRename(t *testing.T) {
usePubKey := false
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.PermChown, dataprovider.PermChtimes}
user, _, err := httpd.AddUser(u, http.StatusOK)
@ -1929,7 +1930,7 @@ func TestPermRename(t *testing.T) {
func TestPermCreateDirs(t *testing.T) {
usePubKey := false
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.PermChown, dataprovider.PermChtimes}
user, _, err := httpd.AddUser(u, http.StatusOK)
@ -1956,7 +1957,7 @@ func TestPermCreateDirs(t *testing.T) {
func TestPermSymlink(t *testing.T) {
usePubKey := false
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.PermChtimes}
user, _, err := httpd.AddUser(u, http.StatusOK)
@ -1999,7 +2000,7 @@ func TestPermSymlink(t *testing.T) {
func TestPermChmod(t *testing.T) {
usePubKey := false
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.PermChown, dataprovider.PermChtimes}
user, _, err := httpd.AddUser(u, http.StatusOK)
@ -2042,7 +2043,7 @@ func TestPermChmod(t *testing.T) {
func TestPermChown(t *testing.T) {
usePubKey := false
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.PermChmod, dataprovider.PermChtimes}
user, _, err := httpd.AddUser(u, http.StatusOK)
@ -2085,7 +2086,7 @@ func TestPermChown(t *testing.T) {
func TestPermChtimes(t *testing.T) {
usePubKey := false
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.PermChmod, dataprovider.PermChown}
user, _, err := httpd.AddUser(u, http.StatusOK)
@ -2125,6 +2126,396 @@ func TestPermChtimes(t *testing.T) {
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) {
usePubKey := false
user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK)
@ -2208,6 +2599,21 @@ func TestSSHFileHash(t *testing.T) {
if err != nil {
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)
if err != nil {
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) {
if len(scpPath) == 0 {
t.Skip("scp command not found, unable to execute this test")
}
usePubKey := true
u := getTestUser(usePubKey)
u.Permissions = []string{dataprovider.PermDownload, dataprovider.PermUpload}
u.Permissions["/"] = []string{dataprovider.PermDownload, dataprovider.PermUpload}
user, _, err := httpd.AddUser(u, http.StatusOK)
if err != nil {
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/")
err = scpUpload(testFilePath, remoteUpPath, true, false)
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)
if err == nil {
@ -2578,7 +3028,7 @@ func TestSCPPermUpload(t *testing.T) {
}
usePubKey := true
u := getTestUser(usePubKey)
u.Permissions = []string{dataprovider.PermDownload, dataprovider.PermCreateDirs}
u.Permissions["/"] = []string{dataprovider.PermDownload, dataprovider.PermCreateDirs}
user, _, err := httpd.AddUser(u, http.StatusOK)
if err != nil {
t.Errorf("unable to add user: %v", err)
@ -2615,7 +3065,7 @@ func TestSCPPermOverwrite(t *testing.T) {
}
usePubKey := true
u := getTestUser(usePubKey)
u.Permissions = []string{dataprovider.PermUpload, dataprovider.PermCreateDirs}
u.Permissions["/"] = []string{dataprovider.PermUpload, dataprovider.PermCreateDirs}
user, _, err := httpd.AddUser(u, http.StatusOK)
if err != nil {
t.Errorf("unable to add user: %v", err)
@ -2656,7 +3106,7 @@ func TestSCPPermDownload(t *testing.T) {
}
usePubKey := true
u := getTestUser(usePubKey)
u.Permissions = []string{dataprovider.PermUpload, dataprovider.PermCreateDirs}
u.Permissions["/"] = []string{dataprovider.PermUpload, dataprovider.PermCreateDirs}
user, _, err := httpd.AddUser(u, http.StatusOK)
if err != nil {
t.Errorf("unable to add user: %v", err)
@ -2668,12 +3118,12 @@ func TestSCPPermDownload(t *testing.T) {
if err != nil {
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)
if err != nil {
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")
err = scpDownload(localPath, remoteDownPath, false, false)
if err == nil {
@ -3008,10 +3458,11 @@ func getTestUser(usePubKey bool) dataprovider.User {
Username: defaultUsername,
Password: defaultPassword,
HomeDir: filepath.Join(homeBasePath, defaultUsername),
Permissions: allPerms,
Status: 1,
ExpirationDate: 0,
}
user.Permissions = make(map[string][]string)
user.Permissions["/"] = allPerms
if usePubKey {
user.PublicKeys = []string{testPubKey}
user.Password = ""
@ -3134,10 +3585,10 @@ func sftpUploadFile(localSourcePath string, remoteDestPath string, expectedSize
return err
}
// 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()
if expectedSize > 0 {
fi, err := client.Lstat(remoteDestPath)
fi, err := client.Stat(remoteDestPath)
if err != nil {
return err
}

View file

@ -129,6 +129,9 @@ func (c *sshCommand) handleHashCommands() error {
if err != nil {
return c.sendErrorResponse(err)
}
if !c.connection.User.HasPerm(dataprovider.PermListItems, path) {
return c.sendErrorResponse(errPermissionDenied)
}
hash, err := computeHashForFile(h, path)
if err != nil {
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,
dataprovider.PermOverwrite, dataprovider.PermDelete, dataprovider.PermRename}
if !c.connection.User.HasPerms(perms) {
if !c.connection.User.HasPerms(perms, command.realPath) {
return c.sendErrorResponse(errPermissionDenied)
}
@ -277,23 +280,6 @@ func (c *sshCommand) getSystemCommand() (systemCommand, error) {
}
args := make([]string, len(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
if len(c.args) > 0 {
var err error
@ -305,6 +291,23 @@ func (c *sshCommand) getSystemCommand() (systemCommand, error) {
args = args[:len(args)-1]
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)
cmd := exec.Command(c.command, args...)
uid := c.connection.User.GetUID()

View file

@ -76,13 +76,28 @@
<select class="form-control" id="idPermissions" name="permissions" required multiple>
{{range $validPerm := .ValidPerms}}
<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>
{{end}}
</select>
</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">
<label for="idHomeDir" class="col-sm-2 col-form-label">Home Dir</label>
<div class="col-sm-10">