mirror of
https://github.com/drakkan/sftpgo.git
synced 2025-12-06 22:30:56 +03:00
add support for users' default base dir
This commit is contained in:
@@ -159,6 +159,7 @@ The `sftpgo` configuration file contains the following sections:
|
|||||||
- 1, quota is updated each time a user upload or delete a file even if the user has no quota restrictions
|
- 1, quota is updated each time a user upload or delete a file even if the user has no quota restrictions
|
||||||
- 2, quota is updated each time a user upload or delete a file but only for users with quota restrictions. With this configuration the "quota scan" REST API can still be used to periodically update space usage for users without quota restrictions
|
- 2, quota is updated each time a user upload or delete a file but only for users with quota restrictions. With this configuration the "quota scan" REST API can still be used to periodically update space usage for users without quota restrictions
|
||||||
- `pool_size`, integer. Sets the maximum number of open connections for `mysql` and `postgresql` driver. Default 0 (unlimited)
|
- `pool_size`, integer. Sets the maximum number of open connections for `mysql` and `postgresql` driver. Default 0 (unlimited)
|
||||||
|
- `users_base_dir`, string. Users' default base directory. If no home dir is defined while adding a new user, and this value is a valid absolute path, then the user home dir will be automatically defined as the path obtained joining the base dir and the username
|
||||||
- **"httpd"**, the configuration for the HTTP server used to serve REST API
|
- **"httpd"**, the configuration for the HTTP server used to serve REST API
|
||||||
- `bind_port`, integer. The port used for serving HTTP requests. Set to 0 to disable HTTP server. Default: 8080
|
- `bind_port`, integer. The port used for serving HTTP requests. Set to 0 to disable HTTP server. Default: 8080
|
||||||
- `bind_address`, string. Leave blank to listen on all available network interfaces. Default: "127.0.0.1"
|
- `bind_address`, string. Leave blank to listen on all available network interfaces. Default: "127.0.0.1"
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const (
|
|||||||
quotaScanPath = "/api/v1/quota_scan"
|
quotaScanPath = "/api/v1/quota_scan"
|
||||||
versionPath = "/api/v1/version"
|
versionPath = "/api/v1/version"
|
||||||
metricsPath = "/metrics"
|
metricsPath = "/metrics"
|
||||||
|
configDir = ".."
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -51,7 +52,6 @@ func TestMain(m *testing.M) {
|
|||||||
} else {
|
} else {
|
||||||
homeBasePath = "/tmp"
|
homeBasePath = "/tmp"
|
||||||
}
|
}
|
||||||
configDir := ".."
|
|
||||||
logfilePath := filepath.Join(configDir, "sftpgo_api_test.log")
|
logfilePath := filepath.Join(configDir, "sftpgo_api_test.log")
|
||||||
logger.InitLogger(logfilePath, 5, 1, 28, false, zerolog.DebugLevel)
|
logger.InitLogger(logfilePath, 5, 1, 28, false, zerolog.DebugLevel)
|
||||||
config.LoadConfig(configDir, "")
|
config.LoadConfig(configDir, "")
|
||||||
@@ -424,6 +424,71 @@ func TestCloseActiveConnection(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUserBaseDir(t *testing.T) {
|
||||||
|
dataProvider := dataprovider.GetProvider()
|
||||||
|
dataprovider.Close(dataProvider)
|
||||||
|
config.LoadConfig(configDir, "")
|
||||||
|
providerConf := config.GetProviderConf()
|
||||||
|
providerConf.UsersBaseDir = homeBasePath
|
||||||
|
err := dataprovider.Initialize(providerConf, configDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error initializing data provider with users base dir")
|
||||||
|
}
|
||||||
|
api.SetDataProvider(dataprovider.GetProvider())
|
||||||
|
u := getTestUser()
|
||||||
|
u.HomeDir = ""
|
||||||
|
user, _, err := api.AddUser(getTestUser(), http.StatusOK)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to add user: %v", err)
|
||||||
|
}
|
||||||
|
if user.HomeDir != filepath.Join(providerConf.UsersBaseDir, u.Username) {
|
||||||
|
t.Errorf("invalid home dir: %v", user.HomeDir)
|
||||||
|
}
|
||||||
|
_, err = api.RemoveUser(user, http.StatusOK)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to remove: %v", err)
|
||||||
|
}
|
||||||
|
dataProvider = dataprovider.GetProvider()
|
||||||
|
dataprovider.Close(dataProvider)
|
||||||
|
config.LoadConfig(configDir, "")
|
||||||
|
providerConf = config.GetProviderConf()
|
||||||
|
err = dataprovider.Initialize(providerConf, configDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error initializing data provider")
|
||||||
|
}
|
||||||
|
api.SetDataProvider(dataprovider.GetProvider())
|
||||||
|
sftpd.SetDataProvider(dataprovider.GetProvider())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProviderErrors(t *testing.T) {
|
||||||
|
dataProvider := dataprovider.GetProvider()
|
||||||
|
dataprovider.Close(dataProvider)
|
||||||
|
_, _, err := api.GetUserByID(0, http.StatusInternalServerError)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("get user with provider closed must fail: %v", err)
|
||||||
|
}
|
||||||
|
_, _, err = api.GetUsers(0, 0, defaultUsername, http.StatusInternalServerError)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("get users with provider closed must fail: %v", err)
|
||||||
|
}
|
||||||
|
_, _, err = api.UpdateUser(dataprovider.User{}, http.StatusInternalServerError)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("update user with provider closed must fail: %v", err)
|
||||||
|
}
|
||||||
|
_, err = api.RemoveUser(dataprovider.User{}, http.StatusInternalServerError)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("delete user with provider closed must fail: %v", err)
|
||||||
|
}
|
||||||
|
config.LoadConfig(configDir, "")
|
||||||
|
providerConf := config.GetProviderConf()
|
||||||
|
err = dataprovider.Initialize(providerConf, configDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error initializing data provider")
|
||||||
|
}
|
||||||
|
api.SetDataProvider(dataprovider.GetProvider())
|
||||||
|
sftpd.SetDataProvider(dataprovider.GetProvider())
|
||||||
|
}
|
||||||
|
|
||||||
// test using mock http server
|
// test using mock http server
|
||||||
|
|
||||||
func TestBasicUserHandlingMock(t *testing.T) {
|
func TestBasicUserHandlingMock(t *testing.T) {
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ func init() {
|
|||||||
SSLMode: 0,
|
SSLMode: 0,
|
||||||
TrackQuota: 1,
|
TrackQuota: 1,
|
||||||
PoolSize: 0,
|
PoolSize: 0,
|
||||||
|
UsersBaseDir: "",
|
||||||
},
|
},
|
||||||
HTTPDConfig: api.HTTPDConf{
|
HTTPDConfig: api.HTTPDConf{
|
||||||
BindPort: 8080,
|
BindPort: 8080,
|
||||||
|
|||||||
@@ -297,6 +297,10 @@ func (p BoltProvider) getUsers(limit int, offset int, order string, username str
|
|||||||
return users, err
|
return users, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p BoltProvider) close() error {
|
||||||
|
return p.dbHandle.Close()
|
||||||
|
}
|
||||||
|
|
||||||
func getUserNoCredentials(user *User) User {
|
func getUserNoCredentials(user *User) User {
|
||||||
user.Password = ""
|
user.Password = ""
|
||||||
user.PublicKeys = []string{}
|
user.PublicKeys = []string{}
|
||||||
|
|||||||
@@ -58,9 +58,10 @@ var (
|
|||||||
PermCreateDirs, PermCreateSymlinks, PermOverwrite}
|
PermCreateDirs, PermCreateSymlinks, PermOverwrite}
|
||||||
hashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix, pbkdf2SHA1Prefix, pbkdf2SHA256Prefix,
|
hashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix, pbkdf2SHA1Prefix, pbkdf2SHA256Prefix,
|
||||||
pbkdf2SHA512Prefix, sha512cryptPwdPrefix}
|
pbkdf2SHA512Prefix, sha512cryptPwdPrefix}
|
||||||
pbkdfPwdPrefixes = []string{pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix}
|
pbkdfPwdPrefixes = []string{pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix}
|
||||||
logSender = "dataProvider"
|
logSender = "dataProvider"
|
||||||
availabilityTicker *time.Ticker
|
availabilityTicker *time.Ticker
|
||||||
|
availabilityTickerDone chan bool
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config provider configuration
|
// Config provider configuration
|
||||||
@@ -100,6 +101,11 @@ type Config struct {
|
|||||||
// Sets the maximum number of open connections for mysql and postgresql driver.
|
// Sets the maximum number of open connections for mysql and postgresql driver.
|
||||||
// Default 0 (unlimited)
|
// Default 0 (unlimited)
|
||||||
PoolSize int `json:"pool_size" mapstructure:"pool_size"`
|
PoolSize int `json:"pool_size" mapstructure:"pool_size"`
|
||||||
|
// Users' default base directory.
|
||||||
|
// If no home dir is defined while adding a new user, and this value is
|
||||||
|
// a valid absolute path, then the user home dir will be automatically
|
||||||
|
// defined as the path obtained joining the base dir and the username
|
||||||
|
UsersBaseDir string `json:"users_base_dir" mapstructure:"users_base_dir"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidationError raised if input data is not valid
|
// ValidationError raised if input data is not valid
|
||||||
@@ -151,6 +157,7 @@ type Provider interface {
|
|||||||
getUsers(limit int, offset int, order string, username string) ([]User, error)
|
getUsers(limit int, offset int, order string, username string) ([]User, error)
|
||||||
getUserByID(ID int64) (User, error)
|
getUserByID(ID int64) (User, error)
|
||||||
checkAvailability() error
|
checkAvailability() error
|
||||||
|
close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -252,7 +259,19 @@ func GetUserByID(p Provider, ID int64) (User, error) {
|
|||||||
return p.getUserByID(ID)
|
return p.getUserByID(ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close releases all database resources
|
||||||
|
func Close(p Provider) error {
|
||||||
|
availabilityTicker.Stop()
|
||||||
|
availabilityTickerDone <- true
|
||||||
|
return p.close()
|
||||||
|
}
|
||||||
|
|
||||||
func validateUser(user *User) error {
|
func validateUser(user *User) error {
|
||||||
|
if len(user.HomeDir) == 0 {
|
||||||
|
if len(config.UsersBaseDir) > 0 {
|
||||||
|
user.HomeDir = filepath.Join(config.UsersBaseDir, user.Username)
|
||||||
|
}
|
||||||
|
}
|
||||||
if len(user.Username) == 0 || len(user.HomeDir) == 0 {
|
if len(user.Username) == 0 || len(user.HomeDir) == 0 {
|
||||||
return &ValidationError{err: "Mandatory parameters missing"}
|
return &ValidationError{err: "Mandatory parameters missing"}
|
||||||
}
|
}
|
||||||
@@ -405,10 +424,16 @@ func getSSLMode() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func startAvailabilityTimer() {
|
func startAvailabilityTimer() {
|
||||||
|
availabilityTickerDone = make(chan bool)
|
||||||
checkDataprovider()
|
checkDataprovider()
|
||||||
go func() {
|
go func() {
|
||||||
for range availabilityTicker.C {
|
for {
|
||||||
checkDataprovider()
|
select {
|
||||||
|
case <-availabilityTickerDone:
|
||||||
|
return
|
||||||
|
case <-availabilityTicker.C:
|
||||||
|
checkDataprovider()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,3 +87,7 @@ func (p MySQLProvider) deleteUser(user User) error {
|
|||||||
func (p MySQLProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) {
|
func (p MySQLProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) {
|
||||||
return sqlCommonGetUsers(limit, offset, order, username, p.dbHandle)
|
return sqlCommonGetUsers(limit, offset, order, username, p.dbHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p MySQLProvider) close() error {
|
||||||
|
return p.dbHandle.Close()
|
||||||
|
}
|
||||||
|
|||||||
@@ -86,3 +86,7 @@ func (p PGSQLProvider) deleteUser(user User) error {
|
|||||||
func (p PGSQLProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) {
|
func (p PGSQLProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) {
|
||||||
return sqlCommonGetUsers(limit, offset, order, username, p.dbHandle)
|
return sqlCommonGetUsers(limit, offset, order, username, p.dbHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p PGSQLProvider) close() error {
|
||||||
|
return p.dbHandle.Close()
|
||||||
|
}
|
||||||
|
|||||||
@@ -93,3 +93,7 @@ func (p SQLiteProvider) deleteUser(user User) error {
|
|||||||
func (p SQLiteProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) {
|
func (p SQLiteProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) {
|
||||||
return sqlCommonGetUsers(limit, offset, order, username, p.dbHandle)
|
return sqlCommonGetUsers(limit, offset, order, username, p.dbHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p SQLiteProvider) close() error {
|
||||||
|
return p.dbHandle.Close()
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ ENV SFTPGO_CONFIG_DIR=${CONFIG_DIR}
|
|||||||
# setting SFTPGO_LOG_FILE_PATH to an empty string will log to stdout
|
# setting SFTPGO_LOG_FILE_PATH to an empty string will log to stdout
|
||||||
ENV SFTPGO_LOG_FILE_PATH=${CONFIG_DIR}/sftpgo.log
|
ENV SFTPGO_LOG_FILE_PATH=${CONFIG_DIR}/sftpgo.log
|
||||||
ENV SFTPGO_HTTPD__BIND_ADDRESS=""
|
ENV SFTPGO_HTTPD__BIND_ADDRESS=""
|
||||||
|
ENV SFTPGO_DATA_PROVIDER__USERS_BASE_DIR=${DATA_DIR}
|
||||||
|
|
||||||
ENTRYPOINT ["sftpgo"]
|
ENTRYPOINT ["sftpgo"]
|
||||||
CMD ["serve"]
|
CMD ["serve"]
|
||||||
@@ -31,7 +31,8 @@
|
|||||||
"users_table": "users",
|
"users_table": "users",
|
||||||
"manage_users": 1,
|
"manage_users": 1,
|
||||||
"track_quota": 2,
|
"track_quota": 2,
|
||||||
"pool_size": 0
|
"pool_size": 0,
|
||||||
|
"users_base_dir": ""
|
||||||
},
|
},
|
||||||
"httpd": {
|
"httpd": {
|
||||||
"bind_port": 8080,
|
"bind_port": 8080,
|
||||||
|
|||||||
Reference in New Issue
Block a user