dataprovider: add support for bbolt key/value store
This way there is an alternative for embedded/small systems if CGO is disabled at build time and so SQLite support cannot be compiled
This commit is contained in:
parent
cb87fe811a
commit
96a39a36bb
7 changed files with 404 additions and 68 deletions
21
README.md
21
README.md
|
@ -7,7 +7,7 @@ Full featured and highly configurable SFTP server software
|
|||
|
||||
- Each account is chrooted to his Home Dir
|
||||
- SFTP accounts are virtual accounts stored in a "data provider"
|
||||
- SQLite, MySQL and PostgreSQL data providers are supported. The `Provider` interface could be extended to support non SQL backends too
|
||||
- SQLite, MySQL, PostgreSQL and bbolt (key/value store in pure Go) data providers are supported
|
||||
- Public key and password authentication
|
||||
- Quota support: accounts can have individual quota expressed as max number of files and max total size
|
||||
- Bandwidth throttling is supported, with distinct settings for upload and download
|
||||
|
@ -28,7 +28,7 @@ Regularly the test cases are manually executed and pass on Windows. Other UNIX v
|
|||
## Requirements
|
||||
|
||||
- Go 1.12 or higher
|
||||
- A suitable SQL server to use as data provider: PostreSQL (9+) or MySQL (5.6+) or SQLite 3.x
|
||||
- A suitable SQL server to use as data provider: PostreSQL (9+) or MySQL (5.6+) or SQLite 3.x or bbolt 1.3.x
|
||||
|
||||
## Installation
|
||||
|
||||
|
@ -45,7 +45,7 @@ On Linux and macOS a compiler is easy to install or already installed, on Window
|
|||
|
||||
The compiler is a build time only dependency, it is not not required at runtime.
|
||||
|
||||
If you don't need SQLite, you can also get/build SFTPGo setting the environment variable `GCO_ENABLED` to 0, this way SQLite support will be disabled but PostgreSQL and MySQL will work and you don't need a `C` compiler for building.
|
||||
If you don't need SQLite, you can also get/build SFTPGo setting the environment variable `GCO_ENABLED` to 0, this way SQLite support will be disabled but PostgreSQL, MySQL and bbolt will work and you don't need a `C` compiler for building.
|
||||
|
||||
Version info, such as git commit and build date, can be embedded setting the following string variables at build time:
|
||||
|
||||
|
@ -90,7 +90,7 @@ Flags:
|
|||
|
||||
The `serve` subcommand supports the following flags:
|
||||
|
||||
- `--config-dir` string. Location of the config dir. This directory should contain the `sftpgo` configuration file and is used as the base for files with a relative path (eg. the private keys for the SFTP server, the SQLite database if you use SQLite as data provider). The default value is "." or the value of `SFTPGO_CONFIG_DIR` environment variable.
|
||||
- `--config-dir` string. Location of the config dir. This directory should contain the `sftpgo` configuration file and is used as the base for files with a relative path (eg. the private keys for the SFTP server, the SQLite or bblot database if you use SQLite or bbolt as data provider). The default value is "." or the value of `SFTPGO_CONFIG_DIR` environment variable.
|
||||
- `--config-file` string. Name of the configuration file. It must be the name of a file stored in config-dir not the absolute path to the configuration file. The specified file name must have no extension we automatically load JSON, YAML, TOML, HCL and Java properties. The default value is "sftpgo" (and therefore `sftpgo.json`, `sftpgo.yaml` and so on are searched) or the value of `SFTPGO_CONFIG_FILE` environment variable
|
||||
- `--log-compress` boolean. Determine if the rotated log files should be compressed using gzip. Default `false` or the value of `SFTPGO_LOG_COMPRESS` environment variable (1 or `true`, 0 or `false`)
|
||||
- `--log-file-path` string. Location for the log file, default "sftpgo.log" or the value of `SFTPGO_LOG_FILE_PATH` environment variable
|
||||
|
@ -130,14 +130,14 @@ The `sftpgo` configuration file contains the following sections:
|
|||
- `keys`, struct array. It contains the daemon's private keys. If empty or missing the daemon will search or try to generate `id_rsa` in the configuration directory.
|
||||
- `private_key`, path to the private key file. It can be a path relative to the config dir or an absolute one.
|
||||
- **"data_provider"**, the configuration for the data provider
|
||||
- `driver`, string. Supported drivers are `sqlite`, `mysql`, `postgresql`
|
||||
- `driver`, string. Supported drivers are `sqlite`, `mysql`, `postgresql`, `bolt`
|
||||
- `name`, string. Database name. For driver `sqlite` this can be the database name relative to the config dir or the absolute path to the SQLite database.
|
||||
- `host`, string. Database host. Leave empty for driver `sqlite`
|
||||
- `port`, integer. Database port. Leave empty for driver `sqlite`
|
||||
- `username`, string. Database user. Leave empty for driver `sqlite`
|
||||
- `password`, string. Database password. Leave empty for driver `sqlite`
|
||||
- `host`, string. Database host. Leave empty for driver `sqlite` and `bolt`
|
||||
- `port`, integer. Database port. Leave empty for driver `sqlite` and `bolt`
|
||||
- `username`, string. Database user. Leave empty for driver `sqlite` and `bolt`
|
||||
- `password`, string. Database password. Leave empty for driver `sqlite` and `bolt`
|
||||
- `sslmode`, integer. Used for drivers `mysql` and `postgresql`. 0 disable SSL/TLS connections, 1 require ssl, 2 set ssl mode to `verify-ca` for driver `postgresql` and `skip-verify` for driver `mysql`, 3 set ssl mode to `verify-full` for driver `postgresql` and `preferred` for driver `mysql`
|
||||
- `connectionstring`, string. Provide a custom database connection string. If not empty this connection string will be used instead of build one using the previous parameters
|
||||
- `connectionstring`, string. Provide a custom database connection string. If not empty this connection string will be used instead of build one using the previous parameters. Leave empty for driver `bolt`
|
||||
- `users_table`, string. Database table for SFTP users
|
||||
- `manage_users`, integer. Set to 0 to disable users management, 1 to enable
|
||||
- `track_quota`, integer. Set the preferred way to track users quota between the following choices:
|
||||
|
@ -325,6 +325,7 @@ The logs can be divided into the following categories:
|
|||
- [argon2id](https://github.com/alexedwards/argon2id)
|
||||
- [go-sqlite3](https://github.com/mattn/go-sqlite3)
|
||||
- [go-sql-driver/mysql](https://github.com/go-sql-driver/mysql)
|
||||
- [bbolt](https://github.com/etcd-io/bbolt)
|
||||
- [lib/pq](https://github.com/lib/pq)
|
||||
- [viper](https://github.com/spf13/viper)
|
||||
- [cobra](https://github.com/spf13/cobra)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
@ -67,7 +66,7 @@ func getUserByID(w http.ResponseWriter, r *http.Request) {
|
|||
user.Password = ""
|
||||
user.PublicKeys = []string{}
|
||||
render.JSON(w, r, user)
|
||||
} else if err == sql.ErrNoRows {
|
||||
} else if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
|
||||
sendAPIResponse(w, r, err, "", http.StatusNotFound)
|
||||
} else {
|
||||
sendAPIResponse(w, r, err, "", http.StatusInternalServerError)
|
||||
|
@ -104,7 +103,7 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
user, err := dataprovider.GetUserByID(dataProvider, userID)
|
||||
if err == sql.ErrNoRows {
|
||||
if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
|
||||
sendAPIResponse(w, r, err, "", http.StatusNotFound)
|
||||
return
|
||||
} else if err != nil {
|
||||
|
@ -136,7 +135,7 @@ func deleteUser(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
user, err := dataprovider.GetUserByID(dataProvider, userID)
|
||||
if err == sql.ErrNoRows {
|
||||
if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
|
||||
sendAPIResponse(w, r, err, "", http.StatusNotFound)
|
||||
return
|
||||
} else if err != nil {
|
||||
|
|
315
dataprovider/bolt.go
Normal file
315
dataprovider/bolt.go
Normal file
|
@ -0,0 +1,315 @@
|
|||
package dataprovider
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
var (
|
||||
usersBucket = []byte("users")
|
||||
usersIDIdxBucket = []byte("users_id_idx")
|
||||
)
|
||||
|
||||
// BoltProvider auth provider for bolt key/value store
|
||||
type BoltProvider struct {
|
||||
dbHandle *bolt.DB
|
||||
}
|
||||
|
||||
func initializeBoltProvider(basePath string) error {
|
||||
var err error
|
||||
dbPath := config.Name
|
||||
if !filepath.IsAbs(dbPath) {
|
||||
dbPath = filepath.Join(basePath, dbPath)
|
||||
}
|
||||
dbHandle, err := bolt.Open(dbPath, 0600, &bolt.Options{
|
||||
NoGrowSync: false,
|
||||
FreelistType: bolt.FreelistArrayType,
|
||||
Timeout: 5 * time.Second})
|
||||
if err == nil {
|
||||
logger.Debug(logSender, "bolt key store handle created")
|
||||
err = dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
_, e := tx.CreateBucketIfNotExists(usersBucket)
|
||||
return e
|
||||
})
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error creating users bucket: %v", err)
|
||||
return err
|
||||
}
|
||||
err = dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
_, e := tx.CreateBucketIfNotExists(usersIDIdxBucket)
|
||||
return e
|
||||
})
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error creating username idx bucket: %v", err)
|
||||
return err
|
||||
}
|
||||
provider = BoltProvider{dbHandle: dbHandle}
|
||||
} else {
|
||||
logger.Warn(logSender, "error creating bolt key/value store handler: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p BoltProvider) validateUserAndPass(username string, password string) (User, error) {
|
||||
var user User
|
||||
if len(password) == 0 {
|
||||
return user, errors.New("Credentials cannot be null or empty")
|
||||
}
|
||||
user, err := p.userExists(username)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error authenticating user: %v, error: %v", username, err)
|
||||
return user, err
|
||||
}
|
||||
return checkUserAndPass(user, password)
|
||||
}
|
||||
|
||||
func (p BoltProvider) validateUserAndPubKey(username string, pubKey string) (User, error) {
|
||||
var user User
|
||||
if len(pubKey) == 0 {
|
||||
return user, errors.New("Credentials cannot be null or empty")
|
||||
}
|
||||
user, err := p.userExists(username)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error authenticating user: %v, error: %v", username, err)
|
||||
return user, err
|
||||
}
|
||||
return checkUserAndPubKey(user, pubKey)
|
||||
}
|
||||
|
||||
func (p BoltProvider) getUserByID(ID int64) (User, error) {
|
||||
var user User
|
||||
err := p.dbHandle.View(func(tx *bolt.Tx) error {
|
||||
bucket, idxBucket, err := getBuckets(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userIDAsBytes := itob(ID)
|
||||
username := idxBucket.Get(userIDAsBytes)
|
||||
if username == nil {
|
||||
return &RecordNotFoundError{err: fmt.Sprintf("user with ID %v does not exist", ID)}
|
||||
}
|
||||
u := bucket.Get(username)
|
||||
if u == nil {
|
||||
return &RecordNotFoundError{err: fmt.Sprintf("username %v and ID: %v does not exist", string(username), ID)}
|
||||
}
|
||||
return json.Unmarshal(u, &user)
|
||||
})
|
||||
|
||||
return user, err
|
||||
}
|
||||
|
||||
func (p BoltProvider) updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error {
|
||||
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
bucket, _, err := getBuckets(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var u []byte
|
||||
if u = bucket.Get([]byte(username)); u == nil {
|
||||
return &RecordNotFoundError{err: fmt.Sprintf("username %v does not exist, unable to update quota", username)}
|
||||
}
|
||||
var user User
|
||||
err = json.Unmarshal(u, &user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if reset {
|
||||
user.UsedQuotaSize = sizeAdd
|
||||
user.UsedQuotaFiles = filesAdd
|
||||
} else {
|
||||
user.UsedQuotaSize += sizeAdd
|
||||
user.UsedQuotaFiles += filesAdd
|
||||
}
|
||||
user.LastQuotaUpdate = utils.GetTimeAsMsSinceEpoch(time.Now())
|
||||
buf, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bucket.Put([]byte(username), buf)
|
||||
})
|
||||
}
|
||||
|
||||
func (p BoltProvider) getUsedQuota(username string) (int, int64, error) {
|
||||
user, err := p.userExists(username)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "unable to get quota for user '%v' error: %v", username, err)
|
||||
return 0, 0, err
|
||||
}
|
||||
return user.UsedQuotaFiles, user.UsedQuotaSize, err
|
||||
}
|
||||
|
||||
func (p BoltProvider) userExists(username string) (User, error) {
|
||||
var user User
|
||||
err := p.dbHandle.View(func(tx *bolt.Tx) error {
|
||||
bucket, _, err := getBuckets(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u := bucket.Get([]byte(username))
|
||||
if u == nil {
|
||||
return &RecordNotFoundError{err: fmt.Sprintf("username %v does not exist", user.Username)}
|
||||
}
|
||||
return json.Unmarshal(u, &user)
|
||||
})
|
||||
return user, err
|
||||
}
|
||||
|
||||
func (p BoltProvider) addUser(user User) error {
|
||||
err := validateUser(&user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
bucket, idxBucket, err := getBuckets(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u := bucket.Get([]byte(user.Username)); u != nil {
|
||||
return fmt.Errorf("username '%v' already exists", user.Username)
|
||||
}
|
||||
id, err := bucket.NextSequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.ID = int64(id)
|
||||
buf, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userIDAsBytes := itob(user.ID)
|
||||
err = bucket.Put([]byte(user.Username), buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return idxBucket.Put(userIDAsBytes, []byte(user.Username))
|
||||
})
|
||||
}
|
||||
|
||||
func (p BoltProvider) updateUser(user User) error {
|
||||
err := validateUser(&user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
bucket, _, err := getBuckets(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u := bucket.Get([]byte(user.Username)); u == nil {
|
||||
return &RecordNotFoundError{err: fmt.Sprintf("username '%v' does not exist", user.Username)}
|
||||
}
|
||||
buf, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bucket.Put([]byte(user.Username), buf)
|
||||
})
|
||||
}
|
||||
|
||||
func (p BoltProvider) deleteUser(user User) error {
|
||||
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||
bucket, idxBucket, err := getBuckets(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userIDAsBytes := itob(user.ID)
|
||||
userName := idxBucket.Get(userIDAsBytes)
|
||||
if userName == nil {
|
||||
return &RecordNotFoundError{err: fmt.Sprintf("user with id %v does not exist", user.ID)}
|
||||
}
|
||||
err = bucket.Delete(userName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return idxBucket.Delete(userIDAsBytes)
|
||||
})
|
||||
}
|
||||
|
||||
func (p BoltProvider) getUsers(limit int, offset int, order string, username string) ([]User, error) {
|
||||
users := []User{}
|
||||
var err error
|
||||
if len(username) > 0 {
|
||||
if offset == 0 {
|
||||
user, err := p.userExists(username)
|
||||
if err == nil {
|
||||
users = append(users, getUserNoCredentials(&user))
|
||||
}
|
||||
}
|
||||
return users, err
|
||||
}
|
||||
err = p.dbHandle.View(func(tx *bolt.Tx) error {
|
||||
if limit <= 0 {
|
||||
return nil
|
||||
}
|
||||
bucket, _, err := getBuckets(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cursor := bucket.Cursor()
|
||||
itNum := 0
|
||||
if order == "ASC" {
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
itNum++
|
||||
if itNum <= offset {
|
||||
continue
|
||||
}
|
||||
var user User
|
||||
err = json.Unmarshal(v, &user)
|
||||
if err == nil {
|
||||
users = append(users, getUserNoCredentials(&user))
|
||||
}
|
||||
if len(users) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for k, v := cursor.Last(); k != nil; k, v = cursor.Prev() {
|
||||
itNum++
|
||||
if itNum <= offset {
|
||||
continue
|
||||
}
|
||||
var user User
|
||||
err = json.Unmarshal(v, &user)
|
||||
if err == nil {
|
||||
users = append(users, getUserNoCredentials(&user))
|
||||
}
|
||||
if len(users) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
})
|
||||
return users, err
|
||||
}
|
||||
|
||||
func getUserNoCredentials(user *User) User {
|
||||
user.Password = ""
|
||||
user.PublicKeys = []string{}
|
||||
return *user
|
||||
}
|
||||
|
||||
// itob returns an 8-byte big endian representation of v.
|
||||
func itob(v int64) []byte {
|
||||
b := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(b, uint64(v))
|
||||
return b
|
||||
}
|
||||
|
||||
func getBuckets(tx *bolt.Tx) (*bolt.Bucket, *bolt.Bucket, error) {
|
||||
var err error
|
||||
bucket := tx.Bucket(usersBucket)
|
||||
idxBucket := tx.Bucket(usersIDIdxBucket)
|
||||
if bucket == nil || idxBucket == nil {
|
||||
err = fmt.Errorf("Unable to find required buckets, bolt database structure not correcly defined")
|
||||
}
|
||||
return bucket, idxBucket, err
|
||||
}
|
|
@ -4,23 +4,28 @@
|
|||
package dataprovider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/alexedwards/argon2id"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
// SQLiteDataProviderName name for sqlite db provider
|
||||
// SQLiteDataProviderName name for SQLite database provider
|
||||
SQLiteDataProviderName = "sqlite"
|
||||
// PGSSQLDataProviderName name for postgresql db provider
|
||||
// PGSSQLDataProviderName name for PostgreSQL database provider
|
||||
PGSSQLDataProviderName = "postgresql"
|
||||
// MySQLDataProviderName name for mysql db provider
|
||||
// MySQLDataProviderName name for MySQL database provider
|
||||
MySQLDataProviderName = "mysql"
|
||||
// BoltDataProviderName name for bbolt key/value store provider
|
||||
BoltDataProviderName = "bolt"
|
||||
|
||||
logSender = "dataProvider"
|
||||
argonPwdPrefix = "$argon2id$"
|
||||
|
@ -31,7 +36,7 @@ const (
|
|||
|
||||
var (
|
||||
// SupportedProviders data provider configured in the sftpgo.conf file must match of these strings
|
||||
SupportedProviders = []string{SQLiteDataProviderName, PGSSQLDataProviderName, MySQLDataProviderName}
|
||||
SupportedProviders = []string{SQLiteDataProviderName, PGSSQLDataProviderName, MySQLDataProviderName, BoltDataProviderName}
|
||||
config Config
|
||||
provider Provider
|
||||
sqlPlaceholders []string
|
||||
|
@ -97,6 +102,15 @@ func (e *MethodDisabledError) Error() string {
|
|||
return fmt.Sprintf("Method disabled error: %s", e.err)
|
||||
}
|
||||
|
||||
// RecordNotFoundError raised if a requested user is not found
|
||||
type RecordNotFoundError struct {
|
||||
err string
|
||||
}
|
||||
|
||||
func (e *RecordNotFoundError) Error() string {
|
||||
return fmt.Sprintf("Not found: %s", e.err)
|
||||
}
|
||||
|
||||
// GetProvider returns the configured provider
|
||||
func GetProvider() Provider {
|
||||
return provider
|
||||
|
@ -127,6 +141,8 @@ func Initialize(cnf Config, basePath string) error {
|
|||
return initializePGSQLProvider()
|
||||
} else if config.Driver == MySQLDataProviderName {
|
||||
return initializeMySQLProvider()
|
||||
} else if config.Driver == BoltDataProviderName {
|
||||
return initializeBoltProvider(basePath)
|
||||
}
|
||||
return fmt.Errorf("Unsupported data provider: %v", config.Driver)
|
||||
}
|
||||
|
@ -238,6 +254,51 @@ func validateUser(user *User) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func checkUserAndPass(user User, password string) (User, error) {
|
||||
var err error
|
||||
if len(user.Password) == 0 {
|
||||
return user, errors.New("Credentials cannot be null or empty")
|
||||
}
|
||||
var match bool
|
||||
if strings.HasPrefix(user.Password, argonPwdPrefix) {
|
||||
match, err = argon2id.ComparePasswordAndHash(password, user.Password)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error comparing password with argon hash: %v", err)
|
||||
return user, err
|
||||
}
|
||||
} else if strings.HasPrefix(user.Password, bcryptPwdPrefix) {
|
||||
if err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
|
||||
logger.Warn(logSender, "error comparing password with bcrypt hash: %v", err)
|
||||
return user, err
|
||||
}
|
||||
match = true
|
||||
} else {
|
||||
// clear text password match
|
||||
match = (user.Password == password)
|
||||
}
|
||||
if !match {
|
||||
err = errors.New("Invalid credentials")
|
||||
}
|
||||
return user, err
|
||||
}
|
||||
|
||||
func checkUserAndPubKey(user User, pubKey string) (User, error) {
|
||||
if len(user.PublicKeys) == 0 {
|
||||
return user, errors.New("Invalid credentials")
|
||||
}
|
||||
for i, k := range user.PublicKeys {
|
||||
storedPubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k))
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error parsing stored public key %d for user %v: %v", i, user.Username, err)
|
||||
return user, err
|
||||
}
|
||||
if string(storedPubKey.Marshal()) == pubKey {
|
||||
return user, nil
|
||||
}
|
||||
}
|
||||
return user, errors.New("Invalid credentials")
|
||||
}
|
||||
|
||||
func getSSLMode() string {
|
||||
if config.Driver == PGSSQLDataProviderName {
|
||||
if config.SSLMode == 0 {
|
||||
|
|
|
@ -4,14 +4,8 @@ import (
|
|||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/alexedwards/argon2id"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
)
|
||||
|
@ -38,34 +32,9 @@ func sqlCommonValidateUserAndPass(username string, password string, dbHandle *sq
|
|||
user, err := getUserByUsername(username, dbHandle)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error authenticating user: %v, error: %v", username, err)
|
||||
} else {
|
||||
// even if the password is empty inside the database an empty user password
|
||||
// will be refused anyway so it cannot match, additional check to be paranoid
|
||||
if len(user.Password) == 0 {
|
||||
return user, errors.New("Credentials cannot be null or empty")
|
||||
}
|
||||
var match bool
|
||||
if strings.HasPrefix(user.Password, argonPwdPrefix) {
|
||||
match, err = argon2id.ComparePasswordAndHash(password, user.Password)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error comparing password with argon hash: %v", err)
|
||||
return user, err
|
||||
}
|
||||
} else if strings.HasPrefix(user.Password, bcryptPwdPrefix) {
|
||||
if err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
|
||||
logger.Warn(logSender, "error comparing password with bcrypt hash: %v", err)
|
||||
return user, err
|
||||
}
|
||||
match = true
|
||||
} else {
|
||||
// clear text password match
|
||||
match = (user.Password == password)
|
||||
}
|
||||
if !match {
|
||||
err = errors.New("Invalid credentials")
|
||||
}
|
||||
return user, err
|
||||
}
|
||||
return user, err
|
||||
return checkUserAndPass(user, password)
|
||||
}
|
||||
|
||||
func sqlCommonValidateUserAndPubKey(username string, pubKey string, dbHandle *sql.DB) (User, error) {
|
||||
|
@ -78,21 +47,7 @@ func sqlCommonValidateUserAndPubKey(username string, pubKey string, dbHandle *sq
|
|||
logger.Warn(logSender, "error authenticating user: %v, error: %v", username, err)
|
||||
return user, err
|
||||
}
|
||||
if len(user.PublicKeys) == 0 {
|
||||
return user, errors.New("Invalid credentials")
|
||||
}
|
||||
|
||||
for i, k := range user.PublicKeys {
|
||||
storedPubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k))
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "error parsing stored public key %d for user %v: %v", i, username, err)
|
||||
return user, err
|
||||
}
|
||||
if string(storedPubKey.Marshal()) == pubKey {
|
||||
return user, nil
|
||||
}
|
||||
}
|
||||
return user, errors.New("Invalid credentials")
|
||||
return checkUserAndPubKey(user, pubKey)
|
||||
}
|
||||
|
||||
func sqlCommonGetUserByID(ID int64, dbHandle *sql.DB) (User, error) {
|
||||
|
@ -241,9 +196,9 @@ func sqlCommonGetUsers(limit int, offset int, order string, username string, dbH
|
|||
for rows.Next() {
|
||||
u, err := getUserFromDbRow(nil, rows)
|
||||
// hide password and public key
|
||||
u.Password = ""
|
||||
u.PublicKeys = []string{}
|
||||
if err == nil {
|
||||
u.Password = ""
|
||||
u.PublicKeys = []string{}
|
||||
users = append(users, u)
|
||||
} else {
|
||||
break
|
||||
|
@ -271,6 +226,9 @@ func getUserFromDbRow(row *sql.Row, rows *sql.Rows) (User, error) {
|
|||
&user.UploadBandwidth, &user.DownloadBandwidth)
|
||||
}
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return user, &RecordNotFoundError{err: err.Error()}
|
||||
}
|
||||
return user, err
|
||||
}
|
||||
if password.Valid {
|
||||
|
|
1
go.mod
1
go.mod
|
@ -17,6 +17,7 @@ require (
|
|||
github.com/spf13/cobra v0.0.5
|
||||
github.com/spf13/viper v1.4.0
|
||||
github.com/stretchr/testify v1.3.0 // indirect
|
||||
go.etcd.io/bbolt v1.3.2
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
)
|
||||
|
|
1
go.sum
1
go.sum
|
@ -128,6 +128,7 @@ github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljT
|
|||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
|
|
Loading…
Reference in a new issue