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:
parent
f8fd5c067c
commit
489101668c
20 changed files with 1166 additions and 273 deletions
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
60
httpd/web.go
60
httpd/web.go
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
55
sftpd/scp.go
55
sftpd/scp.go
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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}}
|
||||
{{- 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">
|
||||
|
|
Loading…
Reference in a new issue