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:
Nicola Murino 2019-08-12 18:31:31 +02:00
parent cb87fe811a
commit 96a39a36bb
7 changed files with 404 additions and 68 deletions

View file

@ -7,7 +7,7 @@ Full featured and highly configurable SFTP server software
- Each account is chrooted to his Home Dir - Each account is chrooted to his Home Dir
- SFTP accounts are virtual accounts stored in a "data provider" - 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 - Public key and password authentication
- Quota support: accounts can have individual quota expressed as max number of files and max total size - 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 - 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 ## Requirements
- Go 1.12 or higher - 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 ## 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. 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: 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: 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 - `--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-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 - `--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. - `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. - `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 - **"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. - `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` - `host`, string. Database host. Leave empty for driver `sqlite` and `bolt`
- `port`, integer. Database port. Leave empty for driver `sqlite` - `port`, integer. Database port. Leave empty for driver `sqlite` and `bolt`
- `username`, string. Database user. Leave empty for driver `sqlite` - `username`, string. Database user. Leave empty for driver `sqlite` and `bolt`
- `password`, string. Database password. Leave empty for driver `sqlite` - `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` - `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 - `users_table`, string. Database table for SFTP users
- `manage_users`, integer. Set to 0 to disable users management, 1 to enable - `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: - `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) - [argon2id](https://github.com/alexedwards/argon2id)
- [go-sqlite3](https://github.com/mattn/go-sqlite3) - [go-sqlite3](https://github.com/mattn/go-sqlite3)
- [go-sql-driver/mysql](https://github.com/go-sql-driver/mysql) - [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) - [lib/pq](https://github.com/lib/pq)
- [viper](https://github.com/spf13/viper) - [viper](https://github.com/spf13/viper)
- [cobra](https://github.com/spf13/cobra) - [cobra](https://github.com/spf13/cobra)

View file

@ -1,7 +1,6 @@
package api package api
import ( import (
"database/sql"
"errors" "errors"
"net/http" "net/http"
"strconv" "strconv"
@ -67,7 +66,7 @@ func getUserByID(w http.ResponseWriter, r *http.Request) {
user.Password = "" user.Password = ""
user.PublicKeys = []string{} user.PublicKeys = []string{}
render.JSON(w, r, user) render.JSON(w, r, user)
} else if err == sql.ErrNoRows { } else if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
sendAPIResponse(w, r, err, "", http.StatusNotFound) sendAPIResponse(w, r, err, "", http.StatusNotFound)
} else { } else {
sendAPIResponse(w, r, err, "", http.StatusInternalServerError) sendAPIResponse(w, r, err, "", http.StatusInternalServerError)
@ -104,7 +103,7 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
return return
} }
user, err := dataprovider.GetUserByID(dataProvider, userID) user, err := dataprovider.GetUserByID(dataProvider, userID)
if err == sql.ErrNoRows { if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
sendAPIResponse(w, r, err, "", http.StatusNotFound) sendAPIResponse(w, r, err, "", http.StatusNotFound)
return return
} else if err != nil { } else if err != nil {
@ -136,7 +135,7 @@ func deleteUser(w http.ResponseWriter, r *http.Request) {
return return
} }
user, err := dataprovider.GetUserByID(dataProvider, userID) user, err := dataprovider.GetUserByID(dataProvider, userID)
if err == sql.ErrNoRows { if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
sendAPIResponse(w, r, err, "", http.StatusNotFound) sendAPIResponse(w, r, err, "", http.StatusNotFound)
return return
} else if err != nil { } else if err != nil {

315
dataprovider/bolt.go Normal file
View 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
}

View file

@ -4,23 +4,28 @@
package dataprovider package dataprovider
import ( import (
"errors"
"fmt" "fmt"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/alexedwards/argon2id" "github.com/alexedwards/argon2id"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/utils" "github.com/drakkan/sftpgo/utils"
) )
const ( const (
// SQLiteDataProviderName name for sqlite db provider // SQLiteDataProviderName name for SQLite database provider
SQLiteDataProviderName = "sqlite" SQLiteDataProviderName = "sqlite"
// PGSSQLDataProviderName name for postgresql db provider // PGSSQLDataProviderName name for PostgreSQL database provider
PGSSQLDataProviderName = "postgresql" PGSSQLDataProviderName = "postgresql"
// MySQLDataProviderName name for mysql db provider // MySQLDataProviderName name for MySQL database provider
MySQLDataProviderName = "mysql" MySQLDataProviderName = "mysql"
// BoltDataProviderName name for bbolt key/value store provider
BoltDataProviderName = "bolt"
logSender = "dataProvider" logSender = "dataProvider"
argonPwdPrefix = "$argon2id$" argonPwdPrefix = "$argon2id$"
@ -31,7 +36,7 @@ const (
var ( var (
// SupportedProviders data provider configured in the sftpgo.conf file must match of these strings // 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 config Config
provider Provider provider Provider
sqlPlaceholders []string sqlPlaceholders []string
@ -97,6 +102,15 @@ func (e *MethodDisabledError) Error() string {
return fmt.Sprintf("Method disabled error: %s", e.err) 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 // GetProvider returns the configured provider
func GetProvider() Provider { func GetProvider() Provider {
return provider return provider
@ -127,6 +141,8 @@ func Initialize(cnf Config, basePath string) error {
return initializePGSQLProvider() return initializePGSQLProvider()
} else if config.Driver == MySQLDataProviderName { } else if config.Driver == MySQLDataProviderName {
return initializeMySQLProvider() return initializeMySQLProvider()
} else if config.Driver == BoltDataProviderName {
return initializeBoltProvider(basePath)
} }
return fmt.Errorf("Unsupported data provider: %v", config.Driver) return fmt.Errorf("Unsupported data provider: %v", config.Driver)
} }
@ -238,6 +254,51 @@ func validateUser(user *User) error {
return nil 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 { func getSSLMode() string {
if config.Driver == PGSSQLDataProviderName { if config.Driver == PGSSQLDataProviderName {
if config.SSLMode == 0 { if config.SSLMode == 0 {

View file

@ -4,14 +4,8 @@ import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"errors" "errors"
"strings"
"time" "time"
"golang.org/x/crypto/ssh"
"github.com/alexedwards/argon2id"
"golang.org/x/crypto/bcrypt"
"github.com/drakkan/sftpgo/logger" "github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/utils" "github.com/drakkan/sftpgo/utils"
) )
@ -38,34 +32,9 @@ func sqlCommonValidateUserAndPass(username string, password string, dbHandle *sq
user, err := getUserByUsername(username, dbHandle) user, err := getUserByUsername(username, dbHandle)
if err != nil { if err != nil {
logger.Warn(logSender, "error authenticating user: %v, error: %v", username, err) logger.Warn(logSender, "error authenticating user: %v, error: %v", username, err)
} else { return user, err
// 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 checkUserAndPass(user, password)
} }
func sqlCommonValidateUserAndPubKey(username string, pubKey string, dbHandle *sql.DB) (User, error) { 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) logger.Warn(logSender, "error authenticating user: %v, error: %v", username, err)
return user, err return user, err
} }
if len(user.PublicKeys) == 0 { return checkUserAndPubKey(user, pubKey)
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")
} }
func sqlCommonGetUserByID(ID int64, dbHandle *sql.DB) (User, error) { 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() { for rows.Next() {
u, err := getUserFromDbRow(nil, rows) u, err := getUserFromDbRow(nil, rows)
// hide password and public key // hide password and public key
u.Password = ""
u.PublicKeys = []string{}
if err == nil { if err == nil {
u.Password = ""
u.PublicKeys = []string{}
users = append(users, u) users = append(users, u)
} else { } else {
break break
@ -271,6 +226,9 @@ func getUserFromDbRow(row *sql.Row, rows *sql.Rows) (User, error) {
&user.UploadBandwidth, &user.DownloadBandwidth) &user.UploadBandwidth, &user.DownloadBandwidth)
} }
if err != nil { if err != nil {
if err == sql.ErrNoRows {
return user, &RecordNotFoundError{err: err.Error()}
}
return user, err return user, err
} }
if password.Valid { if password.Valid {

1
go.mod
View file

@ -17,6 +17,7 @@ require (
github.com/spf13/cobra v0.0.5 github.com/spf13/cobra v0.0.5
github.com/spf13/viper v1.4.0 github.com/spf13/viper v1.4.0
github.com/stretchr/testify v1.3.0 // indirect github.com/stretchr/testify v1.3.0 // indirect
go.etcd.io/bbolt v1.3.2
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0
) )

1
go.sum
View file

@ -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/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/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= 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.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/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=