REST API: add support for API key authentication
This commit is contained in:
parent
05c62b9f40
commit
fe953d6b38
41 changed files with 3620 additions and 274 deletions
|
@ -28,6 +28,7 @@ const (
|
||||||
PermAdminCloseConnections = "close_conns"
|
PermAdminCloseConnections = "close_conns"
|
||||||
PermAdminViewServerStatus = "view_status"
|
PermAdminViewServerStatus = "view_status"
|
||||||
PermAdminManageAdmins = "manage_admins"
|
PermAdminManageAdmins = "manage_admins"
|
||||||
|
PermAdminManageAPIKeys = "manage_apikeys"
|
||||||
PermAdminQuotaScans = "quota_scans"
|
PermAdminQuotaScans = "quota_scans"
|
||||||
PermAdminManageSystem = "manage_system"
|
PermAdminManageSystem = "manage_system"
|
||||||
PermAdminManageDefender = "manage_defender"
|
PermAdminManageDefender = "manage_defender"
|
||||||
|
@ -38,8 +39,8 @@ var (
|
||||||
emailRegex = regexp.MustCompile("^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$")
|
emailRegex = regexp.MustCompile("^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$")
|
||||||
validAdminPerms = []string{PermAdminAny, PermAdminAddUsers, PermAdminChangeUsers, PermAdminDeleteUsers,
|
validAdminPerms = []string{PermAdminAny, PermAdminAddUsers, PermAdminChangeUsers, PermAdminDeleteUsers,
|
||||||
PermAdminViewUsers, PermAdminViewConnections, PermAdminCloseConnections, PermAdminViewServerStatus,
|
PermAdminViewUsers, PermAdminViewConnections, PermAdminCloseConnections, PermAdminViewServerStatus,
|
||||||
PermAdminManageAdmins, PermAdminQuotaScans, PermAdminManageSystem, PermAdminManageDefender,
|
PermAdminManageAdmins, PermAdminManageAPIKeys, PermAdminQuotaScans, PermAdminManageSystem,
|
||||||
PermAdminViewDefender}
|
PermAdminManageDefender, PermAdminViewDefender}
|
||||||
)
|
)
|
||||||
|
|
||||||
// AdminFilters defines additional restrictions for SFTPGo admins
|
// AdminFilters defines additional restrictions for SFTPGo admins
|
||||||
|
@ -49,6 +50,8 @@ type AdminFilters struct {
|
||||||
// IP/Mask must be in CIDR notation as defined in RFC 4632 and RFC 4291
|
// IP/Mask must be in CIDR notation as defined in RFC 4632 and RFC 4291
|
||||||
// for example "192.0.2.0/24" or "2001:db8::/32"
|
// for example "192.0.2.0/24" or "2001:db8::/32"
|
||||||
AllowList []string `json:"allow_list,omitempty"`
|
AllowList []string `json:"allow_list,omitempty"`
|
||||||
|
// API key auth allows to impersonate this administrator with an API key
|
||||||
|
AllowAPIKeyAuth bool `json:"allow_api_key_auth,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admin defines a SFTPGo admin
|
// Admin defines a SFTPGo admin
|
||||||
|
@ -162,10 +165,21 @@ func (a *Admin) CanLoginFromIP(ip string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Admin) checkUserAndPass(password, ip string) error {
|
// CanLogin returns an error if the login is not allowed
|
||||||
|
func (a *Admin) CanLogin(ip string) error {
|
||||||
if a.Status != 1 {
|
if a.Status != 1 {
|
||||||
return fmt.Errorf("admin %#v is disabled", a.Username)
|
return fmt.Errorf("admin %#v is disabled", a.Username)
|
||||||
}
|
}
|
||||||
|
if !a.CanLoginFromIP(ip) {
|
||||||
|
return fmt.Errorf("login from IP %v not allowed", ip)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Admin) checkUserAndPass(password, ip string) error {
|
||||||
|
if err := a.CanLogin(ip); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if a.Password == "" || password == "" {
|
if a.Password == "" || password == "" {
|
||||||
return errors.New("credentials cannot be null or empty")
|
return errors.New("credentials cannot be null or empty")
|
||||||
}
|
}
|
||||||
|
@ -176,9 +190,6 @@ func (a *Admin) checkUserAndPass(password, ip string) error {
|
||||||
if !match {
|
if !match {
|
||||||
return ErrInvalidCredentials
|
return ErrInvalidCredentials
|
||||||
}
|
}
|
||||||
if !a.CanLoginFromIP(ip) {
|
|
||||||
return fmt.Errorf("login from IP %v not allowed", ip)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,6 +247,7 @@ func (a *Admin) getACopy() Admin {
|
||||||
copy(permissions, a.Permissions)
|
copy(permissions, a.Permissions)
|
||||||
filters := AdminFilters{}
|
filters := AdminFilters{}
|
||||||
filters.AllowList = make([]string, len(a.Filters.AllowList))
|
filters.AllowList = make([]string, len(a.Filters.AllowList))
|
||||||
|
filters.AllowAPIKeyAuth = a.Filters.AllowAPIKeyAuth
|
||||||
copy(filters.AllowList, a.Filters.AllowList)
|
copy(filters.AllowList, a.Filters.AllowList)
|
||||||
|
|
||||||
return Admin{
|
return Admin{
|
||||||
|
|
173
dataprovider/apikey.go
Normal file
173
dataprovider/apikey.go
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
package dataprovider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alexedwards/argon2id"
|
||||||
|
"github.com/lithammer/shortuuid/v3"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
|
"github.com/drakkan/sftpgo/v2/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// APIKeyScope defines the supported API key scopes
|
||||||
|
type APIKeyScope int
|
||||||
|
|
||||||
|
// Supported API key scopes
|
||||||
|
const (
|
||||||
|
// the API key will be used for an admin
|
||||||
|
APIKeyScopeAdmin APIKeyScope = iota + 1
|
||||||
|
// the API key will be used for a user
|
||||||
|
APIKeyScopeUser
|
||||||
|
)
|
||||||
|
|
||||||
|
// APIKey defines a SFTPGo API key.
|
||||||
|
// API keys can be used as authentication alternative to short lived tokens
|
||||||
|
// for REST API
|
||||||
|
type APIKey struct {
|
||||||
|
// Database unique identifier
|
||||||
|
ID int64 `json:"-"`
|
||||||
|
// Unique key identifier, used for key lookups.
|
||||||
|
// The generated key is in the format `KeyID.hash(Key)` so we can split
|
||||||
|
// and lookup by KeyID and then verify if the key matches the recorded hash
|
||||||
|
KeyID string `json:"id"`
|
||||||
|
// User friendly key name
|
||||||
|
Name string `json:"name"`
|
||||||
|
// we store the hash of the key, this is just like a password
|
||||||
|
Key string `json:"key,omitempty"`
|
||||||
|
Scope APIKeyScope `json:"scope"`
|
||||||
|
CreatedAt int64 `json:"created_at"`
|
||||||
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
|
// 0 means never used
|
||||||
|
LastUseAt int64 `json:"last_use_at,omitempty"`
|
||||||
|
// 0 means never expire
|
||||||
|
ExpiresAt int64 `json:"expires_at,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
// Username associated with this API key.
|
||||||
|
// If empty and the scope is APIKeyScopeUser the key is valid for any user
|
||||||
|
User string `json:"user,omitempty"`
|
||||||
|
// Admin username associated with this API key.
|
||||||
|
// If empty and the scope is APIKeyScopeAdmin the key is valid for any admin
|
||||||
|
Admin string `json:"admin,omitempty"`
|
||||||
|
// these fields are for internal use
|
||||||
|
userID int64
|
||||||
|
adminID int64
|
||||||
|
plainKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *APIKey) getACopy() APIKey {
|
||||||
|
return APIKey{
|
||||||
|
ID: k.ID,
|
||||||
|
KeyID: k.KeyID,
|
||||||
|
Name: k.Name,
|
||||||
|
Key: k.Key,
|
||||||
|
Scope: k.Scope,
|
||||||
|
CreatedAt: k.CreatedAt,
|
||||||
|
UpdatedAt: k.UpdatedAt,
|
||||||
|
LastUseAt: k.LastUseAt,
|
||||||
|
ExpiresAt: k.ExpiresAt,
|
||||||
|
Description: k.Description,
|
||||||
|
User: k.User,
|
||||||
|
Admin: k.Admin,
|
||||||
|
userID: k.userID,
|
||||||
|
adminID: k.adminID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HideConfidentialData hides admin confidential data
|
||||||
|
func (k *APIKey) HideConfidentialData() {
|
||||||
|
k.Key = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *APIKey) checkKey() error {
|
||||||
|
if k.Key != "" && !util.IsStringPrefixInSlice(k.Key, internalHashPwdPrefixes) {
|
||||||
|
if config.PasswordHashing.Algo == HashingAlgoBcrypt {
|
||||||
|
hashed, err := bcrypt.GenerateFromPassword([]byte(k.Key), config.PasswordHashing.BcryptOptions.Cost)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
k.Key = string(hashed)
|
||||||
|
} else {
|
||||||
|
hashed, err := argon2id.CreateHash(k.Key, argon2Params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
k.Key = hashed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *APIKey) generateKey() {
|
||||||
|
if k.KeyID != "" || k.Key != "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
k.KeyID = shortuuid.New()
|
||||||
|
k.Key = shortuuid.New()
|
||||||
|
k.plainKey = k.Key
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisplayKey returns the key to show to the user
|
||||||
|
func (k *APIKey) DisplayKey() string {
|
||||||
|
return fmt.Sprintf("%v.%v", k.KeyID, k.plainKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *APIKey) validate() error {
|
||||||
|
if k.Name == "" {
|
||||||
|
return util.NewValidationError("name is mandatory")
|
||||||
|
}
|
||||||
|
if k.Scope != APIKeyScopeAdmin && k.Scope != APIKeyScopeUser {
|
||||||
|
return util.NewValidationError(fmt.Sprintf("invalid scope: %v", k.Scope))
|
||||||
|
}
|
||||||
|
k.generateKey()
|
||||||
|
if err := k.checkKey(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if k.CreatedAt == 0 {
|
||||||
|
k.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||||
|
}
|
||||||
|
if k.User != "" && k.Admin != "" {
|
||||||
|
return util.NewValidationError("an API key can be related to a user or an admin, not both")
|
||||||
|
}
|
||||||
|
if k.Scope == APIKeyScopeAdmin {
|
||||||
|
k.User = ""
|
||||||
|
}
|
||||||
|
if k.Scope == APIKeyScopeUser {
|
||||||
|
k.Admin = ""
|
||||||
|
}
|
||||||
|
if k.User != "" {
|
||||||
|
_, err := provider.userExists(k.User)
|
||||||
|
if err != nil {
|
||||||
|
return util.NewValidationError(fmt.Sprintf("unable to check API key user %v: %v", k.User, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if k.Admin != "" {
|
||||||
|
_, err := provider.adminExists(k.Admin)
|
||||||
|
if err != nil {
|
||||||
|
return util.NewValidationError(fmt.Sprintf("unable to check API key admin %v: %v", k.Admin, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate tries to authenticate the provided plain key
|
||||||
|
func (k *APIKey) Authenticate(plainKey string) error {
|
||||||
|
if k.ExpiresAt > 0 && k.ExpiresAt < util.GetTimeAsMsSinceEpoch(time.Now()) {
|
||||||
|
return fmt.Errorf("API key %#v is expired, expiration timestamp: %v current timestamp: %v", k.KeyID,
|
||||||
|
k.ExpiresAt, util.GetTimeAsMsSinceEpoch(time.Now()))
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(k.Key, bcryptPwdPrefix) {
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(k.Key), []byte(plainKey)); err != nil {
|
||||||
|
return ErrInvalidCredentials
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(k.Key, argonPwdPrefix) {
|
||||||
|
match, err := argon2id.ComparePasswordAndHash(plainKey, k.Key)
|
||||||
|
if err != nil || !match {
|
||||||
|
return ErrInvalidCredentials
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -19,13 +19,14 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
boltDatabaseVersion = 10
|
boltDatabaseVersion = 11
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
usersBucket = []byte("users")
|
usersBucket = []byte("users")
|
||||||
foldersBucket = []byte("folders")
|
foldersBucket = []byte("folders")
|
||||||
adminsBucket = []byte("admins")
|
adminsBucket = []byte("admins")
|
||||||
|
apiKeysBucket = []byte("api_keys")
|
||||||
dbVersionBucket = []byte("db_version")
|
dbVersionBucket = []byte("db_version")
|
||||||
dbVersionKey = []byte("version")
|
dbVersionKey = []byte("version")
|
||||||
)
|
)
|
||||||
|
@ -83,6 +84,14 @@ func initializeBoltProvider(basePath string) error {
|
||||||
providerLog(logger.LevelWarn, "error creating admins bucket: %v", err)
|
providerLog(logger.LevelWarn, "error creating admins bucket: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
err = dbHandle.Update(func(tx *bolt.Tx) error {
|
||||||
|
_, e := tx.CreateBucketIfNotExists(apiKeysBucket)
|
||||||
|
return e
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
providerLog(logger.LevelWarn, "error creating api keys bucket: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
err = dbHandle.Update(func(tx *bolt.Tx) error {
|
err = dbHandle.Update(func(tx *bolt.Tx) error {
|
||||||
_, e := tx.CreateBucketIfNotExists(dbVersionBucket)
|
_, e := tx.CreateBucketIfNotExists(dbVersionBucket)
|
||||||
return e
|
return e
|
||||||
|
@ -152,6 +161,36 @@ func (p *BoltProvider) validateUserAndPubKey(username string, pubKey []byte) (Us
|
||||||
return checkUserAndPubKey(&user, pubKey)
|
return checkUserAndPubKey(&user, pubKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *BoltProvider) updateAPIKeyLastUse(keyID string) error {
|
||||||
|
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||||
|
bucket, err := getAPIKeysBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var u []byte
|
||||||
|
if u = bucket.Get([]byte(keyID)); u == nil {
|
||||||
|
return util.NewRecordNotFoundError(fmt.Sprintf("key %#v does not exist, unable to update last use", keyID))
|
||||||
|
}
|
||||||
|
var apiKey APIKey
|
||||||
|
err = json.Unmarshal(u, &apiKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
apiKey.LastUseAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||||
|
buf, err := json.Marshal(apiKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = bucket.Put([]byte(keyID), buf)
|
||||||
|
if err != nil {
|
||||||
|
providerLog(logger.LevelWarn, "error updating last use for key %#v: %v", keyID, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
providerLog(logger.LevelDebug, "last use updated for key %#v", keyID)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (p *BoltProvider) updateLastLogin(username string) error {
|
func (p *BoltProvider) updateLastLogin(username string) error {
|
||||||
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||||
bucket, err := getUsersBucket(tx)
|
bucket, err := getUsersBucket(tx)
|
||||||
|
@ -310,6 +349,10 @@ func (p *BoltProvider) deleteAdmin(admin *Admin) error {
|
||||||
return util.NewRecordNotFoundError(fmt.Sprintf("admin %v does not exist", admin.Username))
|
return util.NewRecordNotFoundError(fmt.Sprintf("admin %v does not exist", admin.Username))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := deleteRelatedAPIKey(tx, admin.Username, APIKeyScopeAdmin); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return bucket.Delete([]byte(admin.Username))
|
return bucket.Delete([]byte(admin.Username))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -503,6 +546,11 @@ func (p *BoltProvider) deleteUser(user *User) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
exists := bucket.Get([]byte(user.Username))
|
||||||
|
if exists == nil {
|
||||||
|
return util.NewRecordNotFoundError(fmt.Sprintf("user %#v does not exist", user.Username))
|
||||||
|
}
|
||||||
|
|
||||||
if len(user.VirtualFolders) > 0 {
|
if len(user.VirtualFolders) > 0 {
|
||||||
folderBucket, err := getFoldersBucket(tx)
|
folderBucket, err := getFoldersBucket(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -515,9 +563,9 @@ func (p *BoltProvider) deleteUser(user *User) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
exists := bucket.Get([]byte(user.Username))
|
|
||||||
if exists == nil {
|
if err := deleteRelatedAPIKey(tx, user.Username, APIKeyScopeUser); err != nil {
|
||||||
return util.NewRecordNotFoundError(fmt.Sprintf("user %#v does not exist", user.Username))
|
return err
|
||||||
}
|
}
|
||||||
return bucket.Delete([]byte(user.Username))
|
return bucket.Delete([]byte(user.Username))
|
||||||
})
|
})
|
||||||
|
@ -833,6 +881,174 @@ func (p *BoltProvider) getUsedFolderQuota(name string) (int, int64, error) {
|
||||||
return folder.UsedQuotaFiles, folder.UsedQuotaSize, err
|
return folder.UsedQuotaFiles, folder.UsedQuotaSize, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *BoltProvider) apiKeyExists(keyID string) (APIKey, error) {
|
||||||
|
var apiKey APIKey
|
||||||
|
err := p.dbHandle.View(func(tx *bolt.Tx) error {
|
||||||
|
bucket, err := getAPIKeysBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
k := bucket.Get([]byte(keyID))
|
||||||
|
if k == nil {
|
||||||
|
return util.NewRecordNotFoundError(fmt.Sprintf("API key %v does not exist", keyID))
|
||||||
|
}
|
||||||
|
return json.Unmarshal(k, &apiKey)
|
||||||
|
})
|
||||||
|
return apiKey, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *BoltProvider) addAPIKey(apiKey *APIKey) error {
|
||||||
|
err := apiKey.validate()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||||
|
bucket, err := getAPIKeysBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if a := bucket.Get([]byte(apiKey.KeyID)); a != nil {
|
||||||
|
return fmt.Errorf("API key %v already exists", apiKey.KeyID)
|
||||||
|
}
|
||||||
|
id, err := bucket.NextSequence()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
apiKey.ID = int64(id)
|
||||||
|
apiKey.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||||
|
buf, err := json.Marshal(apiKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return bucket.Put([]byte(apiKey.KeyID), buf)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *BoltProvider) updateAPIKey(apiKey *APIKey) error {
|
||||||
|
err := apiKey.validate()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||||
|
bucket, err := getAPIKeysBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var a []byte
|
||||||
|
|
||||||
|
if a = bucket.Get([]byte(apiKey.KeyID)); a == nil {
|
||||||
|
return util.NewRecordNotFoundError(fmt.Sprintf("API key %v does not exist", apiKey.KeyID))
|
||||||
|
}
|
||||||
|
var oldAPIKey APIKey
|
||||||
|
err = json.Unmarshal(a, &oldAPIKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
apiKey.ID = oldAPIKey.ID
|
||||||
|
apiKey.KeyID = oldAPIKey.KeyID
|
||||||
|
apiKey.Key = oldAPIKey.Key
|
||||||
|
apiKey.CreatedAt = oldAPIKey.CreatedAt
|
||||||
|
apiKey.LastUseAt = oldAPIKey.LastUseAt
|
||||||
|
apiKey.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||||
|
buf, err := json.Marshal(apiKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return bucket.Put([]byte(apiKey.KeyID), buf)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *BoltProvider) deleteAPIKeys(apiKey *APIKey) error {
|
||||||
|
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||||
|
bucket, err := getAPIKeysBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if bucket.Get([]byte(apiKey.KeyID)) == nil {
|
||||||
|
return util.NewRecordNotFoundError(fmt.Sprintf("API key %v does not exist", apiKey.KeyID))
|
||||||
|
}
|
||||||
|
|
||||||
|
return bucket.Delete([]byte(apiKey.KeyID))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *BoltProvider) getAPIKeys(limit int, offset int, order string) ([]APIKey, error) {
|
||||||
|
apiKeys := make([]APIKey, 0, limit)
|
||||||
|
|
||||||
|
err := p.dbHandle.View(func(tx *bolt.Tx) error {
|
||||||
|
bucket, err := getAPIKeysBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cursor := bucket.Cursor()
|
||||||
|
itNum := 0
|
||||||
|
if order == OrderASC {
|
||||||
|
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||||
|
itNum++
|
||||||
|
if itNum <= offset {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var apiKey APIKey
|
||||||
|
err = json.Unmarshal(v, &apiKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
apiKey.HideConfidentialData()
|
||||||
|
apiKeys = append(apiKeys, apiKey)
|
||||||
|
if len(apiKeys) >= limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for k, v := cursor.Last(); k != nil; k, v = cursor.Prev() {
|
||||||
|
itNum++
|
||||||
|
if itNum <= offset {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var apiKey APIKey
|
||||||
|
err = json.Unmarshal(v, &apiKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
apiKey.HideConfidentialData()
|
||||||
|
apiKeys = append(apiKeys, apiKey)
|
||||||
|
if len(apiKeys) >= limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return apiKeys, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *BoltProvider) dumpAPIKeys() ([]APIKey, error) {
|
||||||
|
apiKeys := make([]APIKey, 0, 30)
|
||||||
|
err := p.dbHandle.View(func(tx *bolt.Tx) error {
|
||||||
|
bucket, err := getAPIKeysBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor := bucket.Cursor()
|
||||||
|
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||||
|
var apiKey APIKey
|
||||||
|
err = json.Unmarshal(v, &apiKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
apiKeys = append(apiKeys, apiKey)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
return apiKeys, err
|
||||||
|
}
|
||||||
|
|
||||||
func (p *BoltProvider) close() error {
|
func (p *BoltProvider) close() error {
|
||||||
return p.dbHandle.Close()
|
return p.dbHandle.Close()
|
||||||
}
|
}
|
||||||
|
@ -860,6 +1076,8 @@ func (p *BoltProvider) migrateDatabase() error {
|
||||||
providerLog(logger.LevelError, "%v", err)
|
providerLog(logger.LevelError, "%v", err)
|
||||||
logger.ErrorToConsole("%v", err)
|
logger.ErrorToConsole("%v", err)
|
||||||
return err
|
return err
|
||||||
|
case version == 10:
|
||||||
|
return updateBoltDatabaseVersion(p.dbHandle, 11)
|
||||||
default:
|
default:
|
||||||
if version > boltDatabaseVersion {
|
if version > boltDatabaseVersion {
|
||||||
providerLog(logger.LevelWarn, "database version %v is newer than the supported one: %v", version,
|
providerLog(logger.LevelWarn, "database version %v is newer than the supported one: %v", version,
|
||||||
|
@ -880,7 +1098,12 @@ func (p *BoltProvider) revertDatabase(targetVersion int) error {
|
||||||
if dbVersion.Version == targetVersion {
|
if dbVersion.Version == targetVersion {
|
||||||
return errors.New("current version match target version, nothing to do")
|
return errors.New("current version match target version, nothing to do")
|
||||||
}
|
}
|
||||||
return errors.New("the current version cannot be reverted")
|
switch dbVersion.Version {
|
||||||
|
case 11:
|
||||||
|
return updateBoltDatabaseVersion(p.dbHandle, 10)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("database version not handled: %v", dbVersion.Version)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func joinUserAndFolders(u []byte, foldersBucket *bolt.Bucket) (User, error) {
|
func joinUserAndFolders(u []byte, foldersBucket *bolt.Bucket) (User, error) {
|
||||||
|
@ -988,12 +1211,55 @@ func removeUserFromFolderMapping(folder *vfs.VirtualFolder, user *User, bucket *
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func deleteRelatedAPIKey(tx *bolt.Tx, username string, scope APIKeyScope) error {
|
||||||
|
bucket, err := getAPIKeysBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var toRemove []string
|
||||||
|
cursor := bucket.Cursor()
|
||||||
|
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||||
|
var apiKey APIKey
|
||||||
|
err = json.Unmarshal(v, &apiKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if scope == APIKeyScopeUser {
|
||||||
|
if apiKey.User == username {
|
||||||
|
toRemove = append(toRemove, apiKey.KeyID)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if apiKey.Admin == username {
|
||||||
|
toRemove = append(toRemove, apiKey.KeyID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, k := range toRemove {
|
||||||
|
if err := bucket.Delete([]byte(k)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAPIKeysBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
bucket := tx.Bucket(apiKeysBucket)
|
||||||
|
if bucket == nil {
|
||||||
|
err = errors.New("unable to find api keys bucket, bolt database structure not correcly defined")
|
||||||
|
}
|
||||||
|
return bucket, err
|
||||||
|
}
|
||||||
|
|
||||||
func getAdminsBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
|
func getAdminsBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
bucket := tx.Bucket(adminsBucket)
|
bucket := tx.Bucket(adminsBucket)
|
||||||
if bucket == nil {
|
if bucket == nil {
|
||||||
err = errors.New("unable to find admin bucket, bolt database structure not correcly defined")
|
err = errors.New("unable to find admins bucket, bolt database structure not correcly defined")
|
||||||
}
|
}
|
||||||
return bucket, err
|
return bucket, err
|
||||||
}
|
}
|
||||||
|
@ -1002,7 +1268,7 @@ func getUsersBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
|
||||||
var err error
|
var err error
|
||||||
bucket := tx.Bucket(usersBucket)
|
bucket := tx.Bucket(usersBucket)
|
||||||
if bucket == nil {
|
if bucket == nil {
|
||||||
err = errors.New("unable to find required buckets, bolt database structure not correcly defined")
|
err = errors.New("unable to find users bucket, bolt database structure not correcly defined")
|
||||||
}
|
}
|
||||||
return bucket, err
|
return bucket, err
|
||||||
}
|
}
|
||||||
|
@ -1011,7 +1277,7 @@ func getFoldersBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
|
||||||
var err error
|
var err error
|
||||||
bucket := tx.Bucket(foldersBucket)
|
bucket := tx.Bucket(foldersBucket)
|
||||||
if bucket == nil {
|
if bucket == nil {
|
||||||
err = fmt.Errorf("unable to find required buckets, bolt database structure not correcly defined")
|
err = fmt.Errorf("unable to find folders buckets, bolt database structure not correcly defined")
|
||||||
}
|
}
|
||||||
return bucket, err
|
return bucket, err
|
||||||
}
|
}
|
||||||
|
@ -1035,7 +1301,7 @@ func getBoltDatabaseVersion(dbHandle *bolt.DB) (schemaVersion, error) {
|
||||||
return dbVersion, err
|
return dbVersion, err
|
||||||
}
|
}
|
||||||
|
|
||||||
/*func updateBoltDatabaseVersion(dbHandle *bolt.DB, version int) error {
|
func updateBoltDatabaseVersion(dbHandle *bolt.DB, version int) error {
|
||||||
err := dbHandle.Update(func(tx *bolt.Tx) error {
|
err := dbHandle.Update(func(tx *bolt.Tx) error {
|
||||||
bucket := tx.Bucket(dbVersionBucket)
|
bucket := tx.Bucket(dbVersionBucket)
|
||||||
if bucket == nil {
|
if bucket == nil {
|
||||||
|
@ -1051,4 +1317,4 @@ func getBoltDatabaseVersion(dbHandle *bolt.DB) (schemaVersion, error) {
|
||||||
return bucket.Put(dbVersionKey, buf)
|
return bucket.Put(dbVersionKey, buf)
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}*/
|
}
|
||||||
|
|
|
@ -69,7 +69,7 @@ const (
|
||||||
CockroachDataProviderName = "cockroachdb"
|
CockroachDataProviderName = "cockroachdb"
|
||||||
// DumpVersion defines the version for the dump.
|
// DumpVersion defines the version for the dump.
|
||||||
// For restore/load we support the current version and the previous one
|
// For restore/load we support the current version and the previous one
|
||||||
DumpVersion = 8
|
DumpVersion = 9
|
||||||
|
|
||||||
argonPwdPrefix = "$argon2id$"
|
argonPwdPrefix = "$argon2id$"
|
||||||
bcryptPwdPrefix = "$2a$"
|
bcryptPwdPrefix = "$2a$"
|
||||||
|
@ -141,6 +141,7 @@ var (
|
||||||
sqlTableFolders = "folders"
|
sqlTableFolders = "folders"
|
||||||
sqlTableFoldersMapping = "folders_mapping"
|
sqlTableFoldersMapping = "folders_mapping"
|
||||||
sqlTableAdmins = "admins"
|
sqlTableAdmins = "admins"
|
||||||
|
sqlTableAPIKeys = "api_keys"
|
||||||
sqlTableSchemaVersion = "schema_version"
|
sqlTableSchemaVersion = "schema_version"
|
||||||
argon2Params *argon2id.Params
|
argon2Params *argon2id.Params
|
||||||
lastLoginMinDelay = 10 * time.Minute
|
lastLoginMinDelay = 10 * time.Minute
|
||||||
|
@ -343,6 +344,7 @@ type BackupData struct {
|
||||||
Users []User `json:"users"`
|
Users []User `json:"users"`
|
||||||
Folders []vfs.BaseVirtualFolder `json:"folders"`
|
Folders []vfs.BaseVirtualFolder `json:"folders"`
|
||||||
Admins []Admin `json:"admins"`
|
Admins []Admin `json:"admins"`
|
||||||
|
APIKeys []APIKey `json:"api_keys"`
|
||||||
Version int `json:"version"`
|
Version int `json:"version"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -405,6 +407,13 @@ type Provider interface {
|
||||||
getAdmins(limit int, offset int, order string) ([]Admin, error)
|
getAdmins(limit int, offset int, order string) ([]Admin, error)
|
||||||
dumpAdmins() ([]Admin, error)
|
dumpAdmins() ([]Admin, error)
|
||||||
validateAdminAndPass(username, password, ip string) (Admin, error)
|
validateAdminAndPass(username, password, ip string) (Admin, error)
|
||||||
|
apiKeyExists(keyID string) (APIKey, error)
|
||||||
|
addAPIKey(apiKey *APIKey) error
|
||||||
|
updateAPIKey(apiKey *APIKey) error
|
||||||
|
deleteAPIKeys(apiKey *APIKey) error
|
||||||
|
getAPIKeys(limit int, offset int, order string) ([]APIKey, error)
|
||||||
|
dumpAPIKeys() ([]APIKey, error)
|
||||||
|
updateAPIKeyLastUse(keyID string) error
|
||||||
checkAvailability() error
|
checkAvailability() error
|
||||||
close() error
|
close() error
|
||||||
reloadConfig() error
|
reloadConfig() error
|
||||||
|
@ -537,9 +546,11 @@ func validateSQLTablesPrefix() error {
|
||||||
sqlTableFolders = config.SQLTablesPrefix + sqlTableFolders
|
sqlTableFolders = config.SQLTablesPrefix + sqlTableFolders
|
||||||
sqlTableFoldersMapping = config.SQLTablesPrefix + sqlTableFoldersMapping
|
sqlTableFoldersMapping = config.SQLTablesPrefix + sqlTableFoldersMapping
|
||||||
sqlTableAdmins = config.SQLTablesPrefix + sqlTableAdmins
|
sqlTableAdmins = config.SQLTablesPrefix + sqlTableAdmins
|
||||||
|
sqlTableAPIKeys = config.SQLTablesPrefix + sqlTableAPIKeys
|
||||||
sqlTableSchemaVersion = config.SQLTablesPrefix + sqlTableSchemaVersion
|
sqlTableSchemaVersion = config.SQLTablesPrefix + sqlTableSchemaVersion
|
||||||
providerLog(logger.LevelDebug, "sql table for users %#v, folders %#v folders mapping %#v admins %#v schema version %#v",
|
providerLog(logger.LevelDebug, "sql table for users %#v, folders %#v folders mapping %#v admins %#v "+
|
||||||
sqlTableUsers, sqlTableFolders, sqlTableFoldersMapping, sqlTableAdmins, sqlTableSchemaVersion)
|
"api keys %#v schema version %#v", sqlTableUsers, sqlTableFolders, sqlTableFoldersMapping, sqlTableAdmins,
|
||||||
|
sqlTableAPIKeys, sqlTableSchemaVersion)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -620,7 +631,7 @@ func CheckCachedUserCredentials(user *CachedUser, password, loginMethod, protoco
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := checkLoginConditions(&user.User); err != nil {
|
if err := user.User.CheckLoginConditions(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if password == "" {
|
if password == "" {
|
||||||
|
@ -791,7 +802,17 @@ func CheckKeyboardInteractiveAuth(username, authHook string, client ssh.Keyboard
|
||||||
return doKeyboardInteractiveAuth(&user, authHook, client, ip, protocol)
|
return doKeyboardInteractiveAuth(&user, authHook, client, ip, protocol)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateLastLogin updates the last login fields for the given SFTP user
|
// UpdateAPIKeyLastUse updates the LastUseAt field for the given API key
|
||||||
|
func UpdateAPIKeyLastUse(apiKey *APIKey) error {
|
||||||
|
lastUse := util.GetTimeFromMsecSinceEpoch(apiKey.LastUseAt)
|
||||||
|
diff := -time.Until(lastUse)
|
||||||
|
if diff < 0 || diff > lastLoginMinDelay {
|
||||||
|
return provider.updateAPIKeyLastUse(apiKey.KeyID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateLastLogin updates the last login field for the given SFTPGo user
|
||||||
func UpdateLastLogin(user *User) error {
|
func UpdateLastLogin(user *User) error {
|
||||||
lastLogin := util.GetTimeFromMsecSinceEpoch(user.LastLogin)
|
lastLogin := util.GetTimeFromMsecSinceEpoch(user.LastLogin)
|
||||||
diff := -time.Until(lastLogin)
|
diff := -time.Until(lastLogin)
|
||||||
|
@ -871,6 +892,33 @@ func GetUsedVirtualFolderQuota(name string) (int, int64, error) {
|
||||||
return files + delayedFiles, size + delayedSize, err
|
return files + delayedFiles, size + delayedSize, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddAPIKey adds a new API key
|
||||||
|
func AddAPIKey(apiKey *APIKey) error {
|
||||||
|
return provider.addAPIKey(apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAPIKey updates an existing API key
|
||||||
|
func UpdateAPIKey(apiKey *APIKey) error {
|
||||||
|
return provider.updateAPIKey(apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAPIKey deletes an existing API key
|
||||||
|
func DeleteAPIKey(keyID string) error {
|
||||||
|
apiKey, err := provider.apiKeyExists(keyID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return provider.deleteAPIKeys(&apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIKeyExists returns the API key with the given ID if it exists
|
||||||
|
func APIKeyExists(keyID string) (APIKey, error) {
|
||||||
|
if keyID == "" {
|
||||||
|
return APIKey{}, util.NewRecordNotFoundError(fmt.Sprintf("API key %#v does not exist", keyID))
|
||||||
|
}
|
||||||
|
return provider.apiKeyExists(keyID)
|
||||||
|
}
|
||||||
|
|
||||||
// HasAdmin returns true if the first admin has been created
|
// HasAdmin returns true if the first admin has been created
|
||||||
// and so SFTPGo is ready to be used
|
// and so SFTPGo is ready to be used
|
||||||
func HasAdmin() bool {
|
func HasAdmin() bool {
|
||||||
|
@ -900,7 +948,7 @@ func DeleteAdmin(username string) error {
|
||||||
return provider.deleteAdmin(&admin)
|
return provider.deleteAdmin(&admin)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminExists returns the given admins if it exists
|
// AdminExists returns the admin with the given username if it exists
|
||||||
func AdminExists(username string) (Admin, error) {
|
func AdminExists(username string) (Admin, error) {
|
||||||
return provider.adminExists(username)
|
return provider.adminExists(username)
|
||||||
}
|
}
|
||||||
|
@ -953,6 +1001,11 @@ func ReloadConfig() error {
|
||||||
return provider.reloadConfig()
|
return provider.reloadConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAPIKeys returns an array of API keys respecting limit and offset
|
||||||
|
func GetAPIKeys(limit, offset int, order string) ([]APIKey, error) {
|
||||||
|
return provider.getAPIKeys(limit, offset, order)
|
||||||
|
}
|
||||||
|
|
||||||
// GetAdmins returns an array of admins respecting limit and offset
|
// GetAdmins returns an array of admins respecting limit and offset
|
||||||
func GetAdmins(limit, offset int, order string) ([]Admin, error) {
|
func GetAdmins(limit, offset int, order string) ([]Admin, error) {
|
||||||
return provider.getAdmins(limit, offset, order)
|
return provider.getAdmins(limit, offset, order)
|
||||||
|
@ -1020,9 +1073,14 @@ func DumpData() (BackupData, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
apiKeys, err := provider.dumpAPIKeys()
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
data.Users = users
|
data.Users = users
|
||||||
data.Folders = folders
|
data.Folders = folders
|
||||||
data.Admins = admins
|
data.Admins = admins
|
||||||
|
data.APIKeys = apiKeys
|
||||||
data.Version = DumpVersion
|
data.Version = DumpVersion
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
@ -1535,17 +1593,6 @@ func ValidateUser(user *User) error {
|
||||||
return saveGCSCredentials(&user.FsConfig, user)
|
return saveGCSCredentials(&user.FsConfig, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkLoginConditions(user *User) error {
|
|
||||||
if user.Status < 1 {
|
|
||||||
return fmt.Errorf("user %#v is disabled", user.Username)
|
|
||||||
}
|
|
||||||
if user.ExpirationDate > 0 && user.ExpirationDate < util.GetTimeAsMsSinceEpoch(time.Now()) {
|
|
||||||
return fmt.Errorf("user %#v is expired, expiration timestamp: %v current timestamp: %v", user.Username,
|
|
||||||
user.ExpirationDate, util.GetTimeAsMsSinceEpoch(time.Now()))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isPasswordOK(user *User, password string) (bool, error) {
|
func isPasswordOK(user *User, password string) (bool, error) {
|
||||||
if config.PasswordCaching {
|
if config.PasswordCaching {
|
||||||
found, match := cachedPasswords.Check(user.Username, password)
|
found, match := cachedPasswords.Check(user.Username, password)
|
||||||
|
@ -1556,17 +1603,17 @@ func isPasswordOK(user *User, password string) (bool, error) {
|
||||||
|
|
||||||
match := false
|
match := false
|
||||||
var err error
|
var err error
|
||||||
if strings.HasPrefix(user.Password, argonPwdPrefix) {
|
if strings.HasPrefix(user.Password, bcryptPwdPrefix) {
|
||||||
|
if err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
|
||||||
|
return match, ErrInvalidCredentials
|
||||||
|
}
|
||||||
|
match = true
|
||||||
|
} else if strings.HasPrefix(user.Password, argonPwdPrefix) {
|
||||||
match, err = argon2id.ComparePasswordAndHash(password, user.Password)
|
match, err = argon2id.ComparePasswordAndHash(password, user.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
providerLog(logger.LevelWarn, "error comparing password with argon hash: %v", err)
|
providerLog(logger.LevelWarn, "error comparing password with argon hash: %v", err)
|
||||||
return match, err
|
return match, err
|
||||||
}
|
}
|
||||||
} else if strings.HasPrefix(user.Password, bcryptPwdPrefix) {
|
|
||||||
if err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
|
|
||||||
return match, ErrInvalidCredentials
|
|
||||||
}
|
|
||||||
match = true
|
|
||||||
} else if util.IsStringPrefixInSlice(user.Password, pbkdfPwdPrefixes) {
|
} else if util.IsStringPrefixInSlice(user.Password, pbkdfPwdPrefixes) {
|
||||||
match, err = comparePbkdf2PasswordAndHash(password, user.Password)
|
match, err = comparePbkdf2PasswordAndHash(password, user.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1585,7 +1632,7 @@ func isPasswordOK(user *User, password string) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkUserAndTLSCertificate(user *User, protocol string, tlsCert *x509.Certificate) (User, error) {
|
func checkUserAndTLSCertificate(user *User, protocol string, tlsCert *x509.Certificate) (User, error) {
|
||||||
err := checkLoginConditions(user)
|
err := user.CheckLoginConditions()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return *user, err
|
return *user, err
|
||||||
}
|
}
|
||||||
|
@ -1604,7 +1651,7 @@ func checkUserAndTLSCertificate(user *User, protocol string, tlsCert *x509.Certi
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkUserAndPass(user *User, password, ip, protocol string) (User, error) {
|
func checkUserAndPass(user *User, password, ip, protocol string) (User, error) {
|
||||||
err := checkLoginConditions(user)
|
err := user.CheckLoginConditions()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return *user, err
|
return *user, err
|
||||||
}
|
}
|
||||||
|
@ -1644,7 +1691,7 @@ func checkUserAndPass(user *User, password, ip, protocol string) (User, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkUserAndPubKey(user *User, pubKey []byte) (User, string, error) {
|
func checkUserAndPubKey(user *User, pubKey []byte) (User, string, error) {
|
||||||
err := checkLoginConditions(user)
|
err := user.CheckLoginConditions()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return *user, "", err
|
return *user, "", err
|
||||||
}
|
}
|
||||||
|
@ -2053,7 +2100,7 @@ func doKeyboardInteractiveAuth(user *User, authHook string, client ssh.KeyboardI
|
||||||
if authResult != 1 {
|
if authResult != 1 {
|
||||||
return *user, fmt.Errorf("keyboard interactive auth failed, result: %v", authResult)
|
return *user, fmt.Errorf("keyboard interactive auth failed, result: %v", authResult)
|
||||||
}
|
}
|
||||||
err = checkLoginConditions(user)
|
err = user.CheckLoginConditions()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return *user, err
|
return *user, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,10 @@ type memoryProviderHandle struct {
|
||||||
admins map[string]Admin
|
admins map[string]Admin
|
||||||
// slice with ordered admins
|
// slice with ordered admins
|
||||||
adminsUsernames []string
|
adminsUsernames []string
|
||||||
|
// map for API keys, keyID is the key
|
||||||
|
apiKeys map[string]APIKey
|
||||||
|
// slice with ordered API keys KeyID
|
||||||
|
apiKeysIDs []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// MemoryProvider auth provider for a memory store
|
// MemoryProvider auth provider for a memory store
|
||||||
|
@ -60,6 +64,8 @@ func initializeMemoryProvider(basePath string) {
|
||||||
vfoldersNames: []string{},
|
vfoldersNames: []string{},
|
||||||
admins: make(map[string]Admin),
|
admins: make(map[string]Admin),
|
||||||
adminsUsernames: []string{},
|
adminsUsernames: []string{},
|
||||||
|
apiKeys: make(map[string]APIKey),
|
||||||
|
apiKeysIDs: []string{},
|
||||||
configFile: configFile,
|
configFile: configFile,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -137,6 +143,21 @@ func (p *MemoryProvider) validateAdminAndPass(username, password, ip string) (Ad
|
||||||
return admin, err
|
return admin, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *MemoryProvider) updateAPIKeyLastUse(keyID string) error {
|
||||||
|
p.dbHandle.Lock()
|
||||||
|
defer p.dbHandle.Unlock()
|
||||||
|
if p.dbHandle.isClosed {
|
||||||
|
return errMemoryProviderClosed
|
||||||
|
}
|
||||||
|
apiKey, err := p.apiKeyExistsInternal(keyID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
apiKey.LastUseAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||||
|
p.dbHandle.apiKeys[apiKey.KeyID] = apiKey
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *MemoryProvider) updateLastLogin(username string) error {
|
func (p *MemoryProvider) updateLastLogin(username string) error {
|
||||||
p.dbHandle.Lock()
|
p.dbHandle.Lock()
|
||||||
defer p.dbHandle.Unlock()
|
defer p.dbHandle.Unlock()
|
||||||
|
@ -273,6 +294,7 @@ func (p *MemoryProvider) deleteUser(user *User) error {
|
||||||
p.dbHandle.usernames = append(p.dbHandle.usernames, username)
|
p.dbHandle.usernames = append(p.dbHandle.usernames, username)
|
||||||
}
|
}
|
||||||
sort.Strings(p.dbHandle.usernames)
|
sort.Strings(p.dbHandle.usernames)
|
||||||
|
p.deleteAPIKeysWithUser(user.Username)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -428,6 +450,7 @@ func (p *MemoryProvider) deleteAdmin(admin *Admin) error {
|
||||||
p.dbHandle.adminsUsernames = append(p.dbHandle.adminsUsernames, username)
|
p.dbHandle.adminsUsernames = append(p.dbHandle.adminsUsernames, username)
|
||||||
}
|
}
|
||||||
sort.Strings(p.dbHandle.adminsUsernames)
|
sort.Strings(p.dbHandle.adminsUsernames)
|
||||||
|
p.deleteAPIKeysWithAdmin(admin.Username)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -770,6 +793,168 @@ func (p *MemoryProvider) deleteFolder(folder *vfs.BaseVirtualFolder) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *MemoryProvider) apiKeyExistsInternal(keyID string) (APIKey, error) {
|
||||||
|
if val, ok := p.dbHandle.apiKeys[keyID]; ok {
|
||||||
|
return val.getACopy(), nil
|
||||||
|
}
|
||||||
|
return APIKey{}, util.NewRecordNotFoundError(fmt.Sprintf("API key %#v does not exist", keyID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MemoryProvider) apiKeyExists(keyID string) (APIKey, error) {
|
||||||
|
p.dbHandle.Lock()
|
||||||
|
defer p.dbHandle.Unlock()
|
||||||
|
if p.dbHandle.isClosed {
|
||||||
|
return APIKey{}, errMemoryProviderClosed
|
||||||
|
}
|
||||||
|
return p.apiKeyExistsInternal(keyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MemoryProvider) addAPIKey(apiKey *APIKey) error {
|
||||||
|
err := apiKey.validate()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.dbHandle.Lock()
|
||||||
|
defer p.dbHandle.Unlock()
|
||||||
|
if p.dbHandle.isClosed {
|
||||||
|
return errMemoryProviderClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = p.apiKeyExistsInternal(apiKey.KeyID)
|
||||||
|
if err == nil {
|
||||||
|
return fmt.Errorf("API key %#v already exists", apiKey.KeyID)
|
||||||
|
}
|
||||||
|
apiKey.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||||
|
p.dbHandle.apiKeys[apiKey.KeyID] = apiKey.getACopy()
|
||||||
|
p.dbHandle.apiKeysIDs = append(p.dbHandle.apiKeysIDs, apiKey.KeyID)
|
||||||
|
sort.Strings(p.dbHandle.apiKeysIDs)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MemoryProvider) updateAPIKey(apiKey *APIKey) error {
|
||||||
|
err := apiKey.validate()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.dbHandle.Lock()
|
||||||
|
defer p.dbHandle.Unlock()
|
||||||
|
if p.dbHandle.isClosed {
|
||||||
|
return errMemoryProviderClosed
|
||||||
|
}
|
||||||
|
k, err := p.apiKeyExistsInternal(apiKey.KeyID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
apiKey.ID = k.ID
|
||||||
|
apiKey.KeyID = k.KeyID
|
||||||
|
apiKey.Key = k.Key
|
||||||
|
apiKey.CreatedAt = k.CreatedAt
|
||||||
|
apiKey.LastUseAt = k.LastUseAt
|
||||||
|
apiKey.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||||
|
p.dbHandle.apiKeys[apiKey.KeyID] = apiKey.getACopy()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MemoryProvider) deleteAPIKeys(apiKey *APIKey) error {
|
||||||
|
p.dbHandle.Lock()
|
||||||
|
defer p.dbHandle.Unlock()
|
||||||
|
if p.dbHandle.isClosed {
|
||||||
|
return errMemoryProviderClosed
|
||||||
|
}
|
||||||
|
_, err := p.apiKeyExistsInternal(apiKey.KeyID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(p.dbHandle.apiKeys, apiKey.KeyID)
|
||||||
|
// this could be more efficient
|
||||||
|
p.dbHandle.apiKeysIDs = make([]string, 0, len(p.dbHandle.apiKeys))
|
||||||
|
for keyID := range p.dbHandle.apiKeys {
|
||||||
|
p.dbHandle.apiKeysIDs = append(p.dbHandle.apiKeysIDs, keyID)
|
||||||
|
}
|
||||||
|
sort.Strings(p.dbHandle.apiKeysIDs)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MemoryProvider) getAPIKeys(limit int, offset int, order string) ([]APIKey, error) {
|
||||||
|
apiKeys := make([]APIKey, 0, limit)
|
||||||
|
|
||||||
|
p.dbHandle.Lock()
|
||||||
|
defer p.dbHandle.Unlock()
|
||||||
|
|
||||||
|
if p.dbHandle.isClosed {
|
||||||
|
return apiKeys, errMemoryProviderClosed
|
||||||
|
}
|
||||||
|
if limit <= 0 {
|
||||||
|
return apiKeys, nil
|
||||||
|
}
|
||||||
|
itNum := 0
|
||||||
|
if order == OrderDESC {
|
||||||
|
for i := len(p.dbHandle.apiKeysIDs) - 1; i >= 0; i-- {
|
||||||
|
itNum++
|
||||||
|
if itNum <= offset {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keyID := p.dbHandle.apiKeysIDs[i]
|
||||||
|
k := p.dbHandle.apiKeys[keyID]
|
||||||
|
apiKey := k.getACopy()
|
||||||
|
apiKey.HideConfidentialData()
|
||||||
|
apiKeys = append(apiKeys, apiKey)
|
||||||
|
if len(apiKeys) >= limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, keyID := range p.dbHandle.apiKeysIDs {
|
||||||
|
itNum++
|
||||||
|
if itNum <= offset {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
k := p.dbHandle.apiKeys[keyID]
|
||||||
|
apiKey := k.getACopy()
|
||||||
|
apiKey.HideConfidentialData()
|
||||||
|
apiKeys = append(apiKeys, apiKey)
|
||||||
|
if len(apiKeys) >= limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiKeys, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MemoryProvider) dumpAPIKeys() ([]APIKey, error) {
|
||||||
|
p.dbHandle.Lock()
|
||||||
|
defer p.dbHandle.Unlock()
|
||||||
|
|
||||||
|
apiKeys := make([]APIKey, 0, len(p.dbHandle.apiKeys))
|
||||||
|
if p.dbHandle.isClosed {
|
||||||
|
return apiKeys, errMemoryProviderClosed
|
||||||
|
}
|
||||||
|
for _, k := range p.dbHandle.apiKeys {
|
||||||
|
apiKeys = append(apiKeys, k)
|
||||||
|
}
|
||||||
|
return apiKeys, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MemoryProvider) deleteAPIKeysWithUser(username string) {
|
||||||
|
for k, v := range p.dbHandle.apiKeys {
|
||||||
|
if v.User == username {
|
||||||
|
delete(p.dbHandle.apiKeys, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MemoryProvider) deleteAPIKeysWithAdmin(username string) {
|
||||||
|
for k, v := range p.dbHandle.apiKeys {
|
||||||
|
if v.Admin == username {
|
||||||
|
delete(p.dbHandle.apiKeys, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (p *MemoryProvider) getNextID() int64 {
|
func (p *MemoryProvider) getNextID() int64 {
|
||||||
nextID := int64(1)
|
nextID := int64(1)
|
||||||
for _, v := range p.dbHandle.users {
|
for _, v := range p.dbHandle.users {
|
||||||
|
|
|
@ -40,6 +40,12 @@ const (
|
||||||
"ALTER TABLE `{{folders_mapping}}` ADD CONSTRAINT `{{prefix}}folders_mapping_folder_id_fk_folders_id` FOREIGN KEY (`folder_id`) REFERENCES `{{folders}}` (`id`) ON DELETE CASCADE;" +
|
"ALTER TABLE `{{folders_mapping}}` ADD CONSTRAINT `{{prefix}}folders_mapping_folder_id_fk_folders_id` FOREIGN KEY (`folder_id`) REFERENCES `{{folders}}` (`id`) ON DELETE CASCADE;" +
|
||||||
"ALTER TABLE `{{folders_mapping}}` ADD CONSTRAINT `{{prefix}}folders_mapping_user_id_fk_users_id` FOREIGN KEY (`user_id`) REFERENCES `{{users}}` (`id`) ON DELETE CASCADE;" +
|
"ALTER TABLE `{{folders_mapping}}` ADD CONSTRAINT `{{prefix}}folders_mapping_user_id_fk_users_id` FOREIGN KEY (`user_id`) REFERENCES `{{users}}` (`id`) ON DELETE CASCADE;" +
|
||||||
"INSERT INTO {{schema_version}} (version) VALUES (10);"
|
"INSERT INTO {{schema_version}} (version) VALUES (10);"
|
||||||
|
mysqlV11SQL = "CREATE TABLE `{{api_keys}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` varchar(255) NOT NULL, `key_id` varchar(50) NOT NULL UNIQUE," +
|
||||||
|
"`api_key` varchar(255) NOT NULL UNIQUE, `scope` integer NOT NULL, `created_at` bigint NOT NULL, `updated_at` bigint NOT NULL, `last_use_at` bigint NOT NULL, " +
|
||||||
|
"`expires_at` bigint NOT NULL, `description` longtext NULL, `admin_id` integer NULL, `user_id` integer NULL);" +
|
||||||
|
"ALTER TABLE `{{api_keys}}` ADD CONSTRAINT `{{prefix}}api_keys_admin_id_fk_admins_id` FOREIGN KEY (`admin_id`) REFERENCES `{{admins}}` (`id`) ON DELETE CASCADE;" +
|
||||||
|
"ALTER TABLE `{{api_keys}}` ADD CONSTRAINT `{{prefix}}api_keys_user_id_fk_users_id` FOREIGN KEY (`user_id`) REFERENCES `{{users}}` (`id`) ON DELETE CASCADE;"
|
||||||
|
mysqlV11DownSQL = "DROP TABLE `{{api_keys}}` CASCADE;"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MySQLProvider auth provider for MySQL/MariaDB database
|
// MySQLProvider auth provider for MySQL/MariaDB database
|
||||||
|
@ -201,6 +207,34 @@ func (p *MySQLProvider) validateAdminAndPass(username, password, ip string) (Adm
|
||||||
return sqlCommonValidateAdminAndPass(username, password, ip, p.dbHandle)
|
return sqlCommonValidateAdminAndPass(username, password, ip, p.dbHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *MySQLProvider) apiKeyExists(keyID string) (APIKey, error) {
|
||||||
|
return sqlCommonGetAPIKeyByID(keyID, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MySQLProvider) addAPIKey(apiKey *APIKey) error {
|
||||||
|
return sqlCommonAddAPIKey(apiKey, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MySQLProvider) updateAPIKey(apiKey *APIKey) error {
|
||||||
|
return sqlCommonUpdateAPIKey(apiKey, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MySQLProvider) deleteAPIKeys(apiKey *APIKey) error {
|
||||||
|
return sqlCommonDeleteAPIKey(apiKey, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MySQLProvider) getAPIKeys(limit int, offset int, order string) ([]APIKey, error) {
|
||||||
|
return sqlCommonGetAPIKeys(limit, offset, order, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MySQLProvider) dumpAPIKeys() ([]APIKey, error) {
|
||||||
|
return sqlCommonDumpAPIKeys(p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MySQLProvider) updateAPIKeyLastUse(keyID string) error {
|
||||||
|
return sqlCommonUpdateAPIKeyLastUse(keyID, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *MySQLProvider) close() error {
|
func (p *MySQLProvider) close() error {
|
||||||
return p.dbHandle.Close()
|
return p.dbHandle.Close()
|
||||||
}
|
}
|
||||||
|
@ -240,6 +274,8 @@ func (p *MySQLProvider) migrateDatabase() error {
|
||||||
providerLog(logger.LevelError, "%v", err)
|
providerLog(logger.LevelError, "%v", err)
|
||||||
logger.ErrorToConsole("%v", err)
|
logger.ErrorToConsole("%v", err)
|
||||||
return err
|
return err
|
||||||
|
case version == 10:
|
||||||
|
return updateMySQLDatabaseFromV10(p.dbHandle)
|
||||||
default:
|
default:
|
||||||
if version > sqlDatabaseVersion {
|
if version > sqlDatabaseVersion {
|
||||||
providerLog(logger.LevelWarn, "database version %v is newer than the supported one: %v", version,
|
providerLog(logger.LevelWarn, "database version %v is newer than the supported one: %v", version,
|
||||||
|
@ -261,5 +297,35 @@ func (p *MySQLProvider) revertDatabase(targetVersion int) error {
|
||||||
return errors.New("current version match target version, nothing to do")
|
return errors.New("current version match target version, nothing to do")
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.New("the current version cannot be reverted")
|
switch dbVersion.Version {
|
||||||
|
case 11:
|
||||||
|
return downgradeMySQLDatabaseFromV11(p.dbHandle)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("database version not handled: %v", dbVersion.Version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateMySQLDatabaseFromV10(dbHandle *sql.DB) error {
|
||||||
|
return updateMySQLDatabaseFrom10To11(dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func downgradeMySQLDatabaseFromV11(dbHandle *sql.DB) error {
|
||||||
|
return downgradeMySQLDatabaseFrom11To10(dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateMySQLDatabaseFrom10To11(dbHandle *sql.DB) error {
|
||||||
|
logger.InfoToConsole("updating database version: 10 -> 11")
|
||||||
|
providerLog(logger.LevelInfo, "updating database version: 10 -> 11")
|
||||||
|
sql := strings.ReplaceAll(mysqlV11SQL, "{{users}}", sqlTableUsers)
|
||||||
|
sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
|
||||||
|
sql = strings.ReplaceAll(sql, "{{api_keys}}", sqlTableAPIKeys)
|
||||||
|
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
|
||||||
|
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 11)
|
||||||
|
}
|
||||||
|
|
||||||
|
func downgradeMySQLDatabaseFrom11To10(dbHandle *sql.DB) error {
|
||||||
|
logger.InfoToConsole("downgrading database version: 11 -> 10")
|
||||||
|
providerLog(logger.LevelInfo, "downgrading database version: 11 -> 10")
|
||||||
|
sql := strings.ReplaceAll(mysqlV11DownSQL, "{{api_keys}}", sqlTableAPIKeys)
|
||||||
|
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 10)
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,18 @@ CREATE INDEX "{{prefix}}folders_mapping_folder_id_idx" ON "{{folders_mapping}}"
|
||||||
CREATE INDEX "{{prefix}}folders_mapping_user_id_idx" ON "{{folders_mapping}}" ("user_id");
|
CREATE INDEX "{{prefix}}folders_mapping_user_id_idx" ON "{{folders_mapping}}" ("user_id");
|
||||||
INSERT INTO {{schema_version}} (version) VALUES (10);
|
INSERT INTO {{schema_version}} (version) VALUES (10);
|
||||||
`
|
`
|
||||||
|
pgsqlV11SQL = `CREATE TABLE "{{api_keys}}" ("id" serial NOT NULL PRIMARY KEY, "name" varchar(255) NOT NULL,
|
||||||
|
"key_id" varchar(50) NOT NULL UNIQUE, "api_key" varchar(255) NOT NULL UNIQUE, "scope" integer NOT NULL,
|
||||||
|
"created_at" bigint NOT NULL, "updated_at" bigint NOT NULL, "last_use_at" bigint NOT NULL,"expires_at" bigint NOT NULL,
|
||||||
|
"description" text NULL, "admin_id" integer NULL, "user_id" integer NULL);
|
||||||
|
ALTER TABLE "{{api_keys}}" ADD CONSTRAINT "{{prefix}}api_keys_admin_id_fk_admins_id" FOREIGN KEY ("admin_id")
|
||||||
|
REFERENCES "{{admins}}" ("id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;
|
||||||
|
ALTER TABLE "{{api_keys}}" ADD CONSTRAINT "{{prefix}}api_keys_user_id_fk_users_id" FOREIGN KEY ("user_id")
|
||||||
|
REFERENCES "{{users}}" ("id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;
|
||||||
|
CREATE INDEX "{{prefix}}api_keys_admin_id_idx" ON "{{api_keys}}" ("admin_id");
|
||||||
|
CREATE INDEX "{{prefix}}api_keys_user_id_idx" ON "{{api_keys}}" ("user_id");
|
||||||
|
`
|
||||||
|
pgsqlV11DownSQL = `DROP TABLE "{{api_keys}}" CASCADE;`
|
||||||
)
|
)
|
||||||
|
|
||||||
// PGSQLProvider auth provider for PostgreSQL database
|
// PGSQLProvider auth provider for PostgreSQL database
|
||||||
|
@ -206,6 +218,34 @@ func (p *PGSQLProvider) validateAdminAndPass(username, password, ip string) (Adm
|
||||||
return sqlCommonValidateAdminAndPass(username, password, ip, p.dbHandle)
|
return sqlCommonValidateAdminAndPass(username, password, ip, p.dbHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *PGSQLProvider) apiKeyExists(keyID string) (APIKey, error) {
|
||||||
|
return sqlCommonGetAPIKeyByID(keyID, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PGSQLProvider) addAPIKey(apiKey *APIKey) error {
|
||||||
|
return sqlCommonAddAPIKey(apiKey, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PGSQLProvider) updateAPIKey(apiKey *APIKey) error {
|
||||||
|
return sqlCommonUpdateAPIKey(apiKey, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PGSQLProvider) deleteAPIKeys(apiKey *APIKey) error {
|
||||||
|
return sqlCommonDeleteAPIKey(apiKey, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PGSQLProvider) getAPIKeys(limit int, offset int, order string) ([]APIKey, error) {
|
||||||
|
return sqlCommonGetAPIKeys(limit, offset, order, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PGSQLProvider) dumpAPIKeys() ([]APIKey, error) {
|
||||||
|
return sqlCommonDumpAPIKeys(p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PGSQLProvider) updateAPIKeyLastUse(keyID string) error {
|
||||||
|
return sqlCommonUpdateAPIKeyLastUse(keyID, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *PGSQLProvider) close() error {
|
func (p *PGSQLProvider) close() error {
|
||||||
return p.dbHandle.Close()
|
return p.dbHandle.Close()
|
||||||
}
|
}
|
||||||
|
@ -251,6 +291,8 @@ func (p *PGSQLProvider) migrateDatabase() error {
|
||||||
providerLog(logger.LevelError, "%v", err)
|
providerLog(logger.LevelError, "%v", err)
|
||||||
logger.ErrorToConsole("%v", err)
|
logger.ErrorToConsole("%v", err)
|
||||||
return err
|
return err
|
||||||
|
case version == 10:
|
||||||
|
return updatePGSQLDatabaseFromV10(p.dbHandle)
|
||||||
default:
|
default:
|
||||||
if version > sqlDatabaseVersion {
|
if version > sqlDatabaseVersion {
|
||||||
providerLog(logger.LevelWarn, "database version %v is newer than the supported one: %v", version,
|
providerLog(logger.LevelWarn, "database version %v is newer than the supported one: %v", version,
|
||||||
|
@ -272,5 +314,35 @@ func (p *PGSQLProvider) revertDatabase(targetVersion int) error {
|
||||||
return errors.New("current version match target version, nothing to do")
|
return errors.New("current version match target version, nothing to do")
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.New("the current version cannot be reverted")
|
switch dbVersion.Version {
|
||||||
|
case 11:
|
||||||
|
return downgradePGSQLDatabaseFromV11(p.dbHandle)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("database version not handled: %v", dbVersion.Version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updatePGSQLDatabaseFromV10(dbHandle *sql.DB) error {
|
||||||
|
return updatePGSQLDatabaseFrom10To11(dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func downgradePGSQLDatabaseFromV11(dbHandle *sql.DB) error {
|
||||||
|
return downgradePGSQLDatabaseFrom11To10(dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updatePGSQLDatabaseFrom10To11(dbHandle *sql.DB) error {
|
||||||
|
logger.InfoToConsole("updating database version: 10 -> 11")
|
||||||
|
providerLog(logger.LevelInfo, "updating database version: 10 -> 11")
|
||||||
|
sql := strings.ReplaceAll(pgsqlV11SQL, "{{users}}", sqlTableUsers)
|
||||||
|
sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
|
||||||
|
sql = strings.ReplaceAll(sql, "{{api_keys}}", sqlTableAPIKeys)
|
||||||
|
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
|
||||||
|
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 11)
|
||||||
|
}
|
||||||
|
|
||||||
|
func downgradePGSQLDatabaseFrom11To10(dbHandle *sql.DB) error {
|
||||||
|
logger.InfoToConsole("downgrading database version: 11 -> 10")
|
||||||
|
providerLog(logger.LevelInfo, "downgrading database version: 11 -> 10")
|
||||||
|
sql := strings.ReplaceAll(pgsqlV11DownSQL, "{{api_keys}}", sqlTableAPIKeys)
|
||||||
|
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 10)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
sqlDatabaseVersion = 10
|
sqlDatabaseVersion = 11
|
||||||
defaultSQLQueryTimeout = 10 * time.Second
|
defaultSQLQueryTimeout = 10 * time.Second
|
||||||
longSQLQueryTimeout = 60 * time.Second
|
longSQLQueryTimeout = 60 * time.Second
|
||||||
)
|
)
|
||||||
|
@ -34,6 +34,170 @@ type sqlScanner interface {
|
||||||
Scan(dest ...interface{}) error
|
Scan(dest ...interface{}) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sqlCommonGetAPIKeyByID(keyID string, dbHandle sqlQuerier) (APIKey, error) {
|
||||||
|
var apiKey APIKey
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
q := getAPIKeyByIDQuery()
|
||||||
|
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||||
|
if err != nil {
|
||||||
|
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
|
||||||
|
return apiKey, err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
row := stmt.QueryRowContext(ctx, keyID)
|
||||||
|
|
||||||
|
apiKey, err = getAPIKeyFromDbRow(row)
|
||||||
|
if err != nil {
|
||||||
|
return apiKey, err
|
||||||
|
}
|
||||||
|
return getAPIKeyWithRelatedFields(ctx, apiKey, dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sqlCommonAddAPIKey(apiKey *APIKey, dbHandle *sql.DB) error {
|
||||||
|
err := apiKey.validate()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, adminID, err := sqlCommonGetAPIKeyRelatedIDs(apiKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
q := getAddAPIKeyQuery()
|
||||||
|
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||||
|
if err != nil {
|
||||||
|
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
_, err = stmt.ExecContext(ctx, apiKey.KeyID, apiKey.Name, apiKey.Key, apiKey.Scope, apiKey.CreatedAt,
|
||||||
|
util.GetTimeAsMsSinceEpoch(time.Now()), apiKey.LastUseAt, apiKey.ExpiresAt, apiKey.Description,
|
||||||
|
userID, adminID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func sqlCommonUpdateAPIKey(apiKey *APIKey, dbHandle *sql.DB) error {
|
||||||
|
err := apiKey.validate()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, adminID, err := sqlCommonGetAPIKeyRelatedIDs(apiKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
q := getUpdateAPIKeyQuery()
|
||||||
|
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||||
|
if err != nil {
|
||||||
|
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
_, err = stmt.ExecContext(ctx, apiKey.Name, apiKey.Scope, apiKey.ExpiresAt, userID, adminID,
|
||||||
|
apiKey.Description, util.GetTimeAsMsSinceEpoch(time.Now()), apiKey.KeyID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func sqlCommonDeleteAPIKey(apiKey *APIKey, dbHandle *sql.DB) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
q := getDeleteAPIKeyQuery()
|
||||||
|
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||||
|
if err != nil {
|
||||||
|
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
_, err = stmt.ExecContext(ctx, apiKey.KeyID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func sqlCommonGetAPIKeys(limit, offset int, order string, dbHandle sqlQuerier) ([]APIKey, error) {
|
||||||
|
apiKeys := make([]APIKey, 0, limit)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
q := getAPIKeysQuery(order)
|
||||||
|
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||||
|
if err != nil {
|
||||||
|
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
rows, err := stmt.QueryContext(ctx, limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
return apiKeys, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
k, err := getAPIKeyFromDbRow(rows)
|
||||||
|
if err != nil {
|
||||||
|
return apiKeys, err
|
||||||
|
}
|
||||||
|
k.HideConfidentialData()
|
||||||
|
apiKeys = append(apiKeys, k)
|
||||||
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
return apiKeys, err
|
||||||
|
}
|
||||||
|
apiKeys, err = getRelatedValuesForAPIKeys(ctx, apiKeys, dbHandle, APIKeyScopeAdmin)
|
||||||
|
if err != nil {
|
||||||
|
return apiKeys, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return getRelatedValuesForAPIKeys(ctx, apiKeys, dbHandle, APIKeyScopeUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sqlCommonDumpAPIKeys(dbHandle sqlQuerier) ([]APIKey, error) {
|
||||||
|
apiKeys := make([]APIKey, 0, 30)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
q := getDumpAPIKeysQuery()
|
||||||
|
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||||
|
if err != nil {
|
||||||
|
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
rows, err := stmt.QueryContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return apiKeys, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
k, err := getAPIKeyFromDbRow(rows)
|
||||||
|
if err != nil {
|
||||||
|
return apiKeys, err
|
||||||
|
}
|
||||||
|
apiKeys = append(apiKeys, k)
|
||||||
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
return apiKeys, err
|
||||||
|
}
|
||||||
|
apiKeys, err = getRelatedValuesForAPIKeys(ctx, apiKeys, dbHandle, APIKeyScopeAdmin)
|
||||||
|
if err != nil {
|
||||||
|
return apiKeys, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return getRelatedValuesForAPIKeys(ctx, apiKeys, dbHandle, APIKeyScopeUser)
|
||||||
|
}
|
||||||
|
|
||||||
func sqlCommonGetAdminByUsername(username string, dbHandle sqlQuerier) (Admin, error) {
|
func sqlCommonGetAdminByUsername(username string, dbHandle sqlQuerier) (Admin, error) {
|
||||||
var admin Admin
|
var admin Admin
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||||
|
@ -303,6 +467,25 @@ func sqlCommonGetUsedQuota(username string, dbHandle *sql.DB) (int, int64, error
|
||||||
return usedFiles, usedSize, err
|
return usedFiles, usedSize, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sqlCommonUpdateAPIKeyLastUse(keyID string, dbHandle *sql.DB) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
q := getUpdateAPIKeyLastUseQuery()
|
||||||
|
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||||
|
if err != nil {
|
||||||
|
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
_, err = stmt.ExecContext(ctx, util.GetTimeAsMsSinceEpoch(time.Now()), keyID)
|
||||||
|
if err == nil {
|
||||||
|
providerLog(logger.LevelDebug, "last use updated for key %#v", keyID)
|
||||||
|
} else {
|
||||||
|
providerLog(logger.LevelWarn, "error updating last use for key %#v: %v", keyID, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func sqlCommonUpdateLastLogin(username string, dbHandle *sql.DB) error {
|
func sqlCommonUpdateLastLogin(username string, dbHandle *sql.DB) error {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
@ -486,6 +669,34 @@ func sqlCommonGetUsers(limit int, offset int, order string, dbHandle sqlQuerier)
|
||||||
return getUsersWithVirtualFolders(ctx, users, dbHandle)
|
return getUsersWithVirtualFolders(ctx, users, dbHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getAPIKeyFromDbRow(row sqlScanner) (APIKey, error) {
|
||||||
|
var apiKey APIKey
|
||||||
|
var userID, adminID sql.NullInt64
|
||||||
|
var description sql.NullString
|
||||||
|
|
||||||
|
err := row.Scan(&apiKey.KeyID, &apiKey.Name, &apiKey.Key, &apiKey.Scope, &apiKey.CreatedAt, &apiKey.UpdatedAt,
|
||||||
|
&apiKey.LastUseAt, &apiKey.ExpiresAt, &description, &userID, &adminID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return apiKey, util.NewRecordNotFoundError(err.Error())
|
||||||
|
}
|
||||||
|
return apiKey, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if userID.Valid {
|
||||||
|
apiKey.userID = userID.Int64
|
||||||
|
}
|
||||||
|
if adminID.Valid {
|
||||||
|
apiKey.adminID = adminID.Int64
|
||||||
|
}
|
||||||
|
if description.Valid {
|
||||||
|
apiKey.Description = description.String
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
func getAdminFromDbRow(row sqlScanner) (Admin, error) {
|
func getAdminFromDbRow(row sqlScanner) (Admin, error) {
|
||||||
var admin Admin
|
var admin Admin
|
||||||
var email, filters, additionalInfo, permissions, description sql.NullString
|
var email, filters, additionalInfo, permissions, description sql.NullString
|
||||||
|
@ -526,7 +737,7 @@ func getAdminFromDbRow(row sqlScanner) (Admin, error) {
|
||||||
admin.Description = description.String
|
admin.Description = description.String
|
||||||
}
|
}
|
||||||
|
|
||||||
return admin, err
|
return admin, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUserFromDbRow(row sqlScanner) (User, error) {
|
func getUserFromDbRow(row sqlScanner) (User, error) {
|
||||||
|
@ -565,7 +776,7 @@ func getUserFromDbRow(row sqlScanner) (User, error) {
|
||||||
perms := make(map[string][]string)
|
perms := make(map[string][]string)
|
||||||
err = json.Unmarshal([]byte(permissions.String), &perms)
|
err = json.Unmarshal([]byte(permissions.String), &perms)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
providerLog(logger.LevelDebug, "unable to deserialize permissions for user %#v: %v", user.Username, err)
|
providerLog(logger.LevelWarn, "unable to deserialize permissions for user %#v: %v", user.Username, err)
|
||||||
return user, fmt.Errorf("unable to deserialize permissions for user %#v: %v", user.Username, err)
|
return user, fmt.Errorf("unable to deserialize permissions for user %#v: %v", user.Username, err)
|
||||||
}
|
}
|
||||||
user.Permissions = perms
|
user.Permissions = perms
|
||||||
|
@ -591,7 +802,7 @@ func getUserFromDbRow(row sqlScanner) (User, error) {
|
||||||
user.Description = description.String
|
user.Description = description.String
|
||||||
}
|
}
|
||||||
user.SetEmptySecretsIfNil()
|
user.SetEmptySecretsIfNil()
|
||||||
return user, err
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func sqlCommonCheckFolderExists(ctx context.Context, name string, dbHandle sqlQuerier) error {
|
func sqlCommonCheckFolderExists(ctx context.Context, name string, dbHandle sqlQuerier) error {
|
||||||
|
@ -890,11 +1101,12 @@ func getUserWithVirtualFolders(ctx context.Context, user User, dbHandle sqlQueri
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUsersWithVirtualFolders(ctx context.Context, users []User, dbHandle sqlQuerier) ([]User, error) {
|
func getUsersWithVirtualFolders(ctx context.Context, users []User, dbHandle sqlQuerier) ([]User, error) {
|
||||||
|
if len(users) == 0 {
|
||||||
|
return users, nil
|
||||||
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
usersVirtualFolders := make(map[int64][]vfs.VirtualFolder)
|
usersVirtualFolders := make(map[int64][]vfs.VirtualFolder)
|
||||||
if len(users) == 0 {
|
|
||||||
return users, err
|
|
||||||
}
|
|
||||||
q := getRelatedFoldersForUsersQuery(users)
|
q := getRelatedFoldersForUsersQuery(users)
|
||||||
stmt, err := dbHandle.PrepareContext(ctx, q)
|
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -947,11 +1159,12 @@ func getUsersWithVirtualFolders(ctx context.Context, users []User, dbHandle sqlQ
|
||||||
}
|
}
|
||||||
|
|
||||||
func getVirtualFoldersWithUsers(folders []vfs.BaseVirtualFolder, dbHandle sqlQuerier) ([]vfs.BaseVirtualFolder, error) {
|
func getVirtualFoldersWithUsers(folders []vfs.BaseVirtualFolder, dbHandle sqlQuerier) ([]vfs.BaseVirtualFolder, error) {
|
||||||
|
if len(folders) == 0 {
|
||||||
|
return folders, nil
|
||||||
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
vFoldersUsers := make(map[int64][]string)
|
vFoldersUsers := make(map[int64][]string)
|
||||||
if len(folders) == 0 {
|
|
||||||
return folders, err
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
q := getRelatedUsersForFoldersQuery(folders)
|
q := getRelatedUsersForFoldersQuery(folders)
|
||||||
|
@ -1030,6 +1243,94 @@ func sqlCommonGetFolderUsedQuota(mappedPath string, dbHandle *sql.DB) (int, int6
|
||||||
return usedFiles, usedSize, err
|
return usedFiles, usedSize, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getAPIKeyWithRelatedFields(ctx context.Context, apiKey APIKey, dbHandle sqlQuerier) (APIKey, error) {
|
||||||
|
var apiKeys []APIKey
|
||||||
|
var err error
|
||||||
|
|
||||||
|
scope := APIKeyScopeAdmin
|
||||||
|
if apiKey.userID > 0 {
|
||||||
|
scope = APIKeyScopeUser
|
||||||
|
}
|
||||||
|
apiKeys, err = getRelatedValuesForAPIKeys(ctx, []APIKey{apiKey}, dbHandle, scope)
|
||||||
|
if err != nil {
|
||||||
|
return apiKey, err
|
||||||
|
}
|
||||||
|
if len(apiKeys) > 0 {
|
||||||
|
apiKey = apiKeys[0]
|
||||||
|
}
|
||||||
|
return apiKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRelatedValuesForAPIKeys(ctx context.Context, apiKeys []APIKey, dbHandle sqlQuerier, scope APIKeyScope) ([]APIKey, error) {
|
||||||
|
if len(apiKeys) == 0 {
|
||||||
|
return apiKeys, nil
|
||||||
|
}
|
||||||
|
values := make(map[int64]string)
|
||||||
|
var q string
|
||||||
|
if scope == APIKeyScopeUser {
|
||||||
|
q = getRelatedUsersForAPIKeysQuery(apiKeys)
|
||||||
|
} else {
|
||||||
|
q = getRelatedAdminsForAPIKeysQuery(apiKeys)
|
||||||
|
}
|
||||||
|
stmt, err := dbHandle.PrepareContext(ctx, q)
|
||||||
|
if err != nil {
|
||||||
|
providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
rows, err := stmt.QueryContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
var valueID int64
|
||||||
|
var valueName string
|
||||||
|
err = rows.Scan(&valueID, &valueName)
|
||||||
|
if err != nil {
|
||||||
|
return apiKeys, err
|
||||||
|
}
|
||||||
|
values[valueID] = valueName
|
||||||
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
return apiKeys, err
|
||||||
|
}
|
||||||
|
if len(values) == 0 {
|
||||||
|
return apiKeys, nil
|
||||||
|
}
|
||||||
|
for idx := range apiKeys {
|
||||||
|
ref := &apiKeys[idx]
|
||||||
|
if scope == APIKeyScopeUser {
|
||||||
|
ref.User = values[ref.userID]
|
||||||
|
} else {
|
||||||
|
ref.Admin = values[ref.adminID]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return apiKeys, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sqlCommonGetAPIKeyRelatedIDs(apiKey *APIKey) (sql.NullInt64, sql.NullInt64, error) {
|
||||||
|
var userID, adminID sql.NullInt64
|
||||||
|
if apiKey.User != "" {
|
||||||
|
u, err := provider.userExists(apiKey.User)
|
||||||
|
if err != nil {
|
||||||
|
return userID, adminID, util.NewValidationError(fmt.Sprintf("unable to validate user %v", apiKey.User))
|
||||||
|
}
|
||||||
|
userID.Valid = true
|
||||||
|
userID.Int64 = u.ID
|
||||||
|
}
|
||||||
|
if apiKey.Admin != "" {
|
||||||
|
a, err := provider.adminExists(apiKey.Admin)
|
||||||
|
if err != nil {
|
||||||
|
return userID, adminID, util.NewValidationError(fmt.Sprintf("unable to validate admin %v", apiKey.Admin))
|
||||||
|
}
|
||||||
|
adminID.Valid = true
|
||||||
|
adminID.Int64 = a.ID
|
||||||
|
}
|
||||||
|
return userID, adminID, nil
|
||||||
|
}
|
||||||
|
|
||||||
func sqlCommonGetDatabaseVersion(dbHandle *sql.DB, showInitWarn bool) (schemaVersion, error) {
|
func sqlCommonGetDatabaseVersion(dbHandle *sql.DB, showInitWarn bool) (schemaVersion, error) {
|
||||||
var result schemaVersion
|
var result schemaVersion
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||||
|
|
|
@ -43,6 +43,15 @@ CREATE INDEX "{{prefix}}folders_mapping_folder_id_idx" ON "{{folders_mapping}}"
|
||||||
CREATE INDEX "{{prefix}}folders_mapping_user_id_idx" ON "{{folders_mapping}}" ("user_id");
|
CREATE INDEX "{{prefix}}folders_mapping_user_id_idx" ON "{{folders_mapping}}" ("user_id");
|
||||||
INSERT INTO {{schema_version}} (version) VALUES (10);
|
INSERT INTO {{schema_version}} (version) VALUES (10);
|
||||||
`
|
`
|
||||||
|
sqliteV11SQL = `CREATE TABLE "{{api_keys}}" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(255) NOT NULL,
|
||||||
|
"key_id" varchar(50) NOT NULL UNIQUE, "api_key" varchar(255) NOT NULL UNIQUE, "scope" integer NOT NULL, "created_at" bigint NOT NULL,
|
||||||
|
"updated_at" bigint NOT NULL, "last_use_at" bigint NOT NULL, "expires_at" bigint NOT NULL, "description" text NULL,
|
||||||
|
"admin_id" integer NULL REFERENCES "{{admins}}" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
|
||||||
|
"user_id" integer NULL REFERENCES "{{users}}" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED);
|
||||||
|
CREATE INDEX "{{prefix}}api_keys_admin_id_idx" ON "api_keys" ("admin_id");
|
||||||
|
CREATE INDEX "{{prefix}}api_keys_user_id_idx" ON "api_keys" ("user_id");
|
||||||
|
`
|
||||||
|
sqliteV11DownSQL = `DROP TABLE "{{api_keys}}";`
|
||||||
)
|
)
|
||||||
|
|
||||||
// SQLiteProvider auth provider for SQLite database
|
// SQLiteProvider auth provider for SQLite database
|
||||||
|
@ -196,6 +205,34 @@ func (p *SQLiteProvider) validateAdminAndPass(username, password, ip string) (Ad
|
||||||
return sqlCommonValidateAdminAndPass(username, password, ip, p.dbHandle)
|
return sqlCommonValidateAdminAndPass(username, password, ip, p.dbHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *SQLiteProvider) apiKeyExists(keyID string) (APIKey, error) {
|
||||||
|
return sqlCommonGetAPIKeyByID(keyID, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SQLiteProvider) addAPIKey(apiKey *APIKey) error {
|
||||||
|
return sqlCommonAddAPIKey(apiKey, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SQLiteProvider) updateAPIKey(apiKey *APIKey) error {
|
||||||
|
return sqlCommonUpdateAPIKey(apiKey, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SQLiteProvider) deleteAPIKeys(apiKey *APIKey) error {
|
||||||
|
return sqlCommonDeleteAPIKey(apiKey, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SQLiteProvider) getAPIKeys(limit int, offset int, order string) ([]APIKey, error) {
|
||||||
|
return sqlCommonGetAPIKeys(limit, offset, order, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SQLiteProvider) dumpAPIKeys() ([]APIKey, error) {
|
||||||
|
return sqlCommonDumpAPIKeys(p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SQLiteProvider) updateAPIKeyLastUse(keyID string) error {
|
||||||
|
return sqlCommonUpdateAPIKeyLastUse(keyID, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *SQLiteProvider) close() error {
|
func (p *SQLiteProvider) close() error {
|
||||||
return p.dbHandle.Close()
|
return p.dbHandle.Close()
|
||||||
}
|
}
|
||||||
|
@ -235,6 +272,8 @@ func (p *SQLiteProvider) migrateDatabase() error {
|
||||||
providerLog(logger.LevelError, "%v", err)
|
providerLog(logger.LevelError, "%v", err)
|
||||||
logger.ErrorToConsole("%v", err)
|
logger.ErrorToConsole("%v", err)
|
||||||
return err
|
return err
|
||||||
|
case version == 10:
|
||||||
|
return updateSQLiteDatabaseFromV10(p.dbHandle)
|
||||||
default:
|
default:
|
||||||
if version > sqlDatabaseVersion {
|
if version > sqlDatabaseVersion {
|
||||||
providerLog(logger.LevelWarn, "database version %v is newer than the supported one: %v", version,
|
providerLog(logger.LevelWarn, "database version %v is newer than the supported one: %v", version,
|
||||||
|
@ -256,7 +295,37 @@ func (p *SQLiteProvider) revertDatabase(targetVersion int) error {
|
||||||
return errors.New("current version match target version, nothing to do")
|
return errors.New("current version match target version, nothing to do")
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.New("the current version cannot be reverted")
|
switch dbVersion.Version {
|
||||||
|
case 11:
|
||||||
|
return downgradeSQLiteDatabaseFromV11(p.dbHandle)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("database version not handled: %v", dbVersion.Version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateSQLiteDatabaseFromV10(dbHandle *sql.DB) error {
|
||||||
|
return updateSQLiteDatabaseFrom10To11(dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func downgradeSQLiteDatabaseFromV11(dbHandle *sql.DB) error {
|
||||||
|
return downgradeSQLiteDatabaseFrom11To10(dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateSQLiteDatabaseFrom10To11(dbHandle *sql.DB) error {
|
||||||
|
logger.InfoToConsole("updating database version: 10 -> 11")
|
||||||
|
providerLog(logger.LevelInfo, "updating database version: 10 -> 11")
|
||||||
|
sql := strings.ReplaceAll(sqliteV11SQL, "{{users}}", sqlTableUsers)
|
||||||
|
sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
|
||||||
|
sql = strings.ReplaceAll(sql, "{{api_keys}}", sqlTableAPIKeys)
|
||||||
|
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
|
||||||
|
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 11)
|
||||||
|
}
|
||||||
|
|
||||||
|
func downgradeSQLiteDatabaseFrom11To10(dbHandle *sql.DB) error {
|
||||||
|
logger.InfoToConsole("downgrading database version: 11 -> 10")
|
||||||
|
providerLog(logger.LevelInfo, "downgrading database version: 11 -> 10")
|
||||||
|
sql := strings.ReplaceAll(sqliteV11DownSQL, "{{api_keys}}", sqlTableAPIKeys)
|
||||||
|
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*func setPragmaFK(dbHandle *sql.DB, value string) error {
|
/*func setPragmaFK(dbHandle *sql.DB, value string) error {
|
||||||
|
|
|
@ -14,6 +14,7 @@ const (
|
||||||
"additional_info,description"
|
"additional_info,description"
|
||||||
selectFolderFields = "id,path,used_quota_size,used_quota_files,last_quota_update,name,description,filesystem"
|
selectFolderFields = "id,path,used_quota_size,used_quota_files,last_quota_update,name,description,filesystem"
|
||||||
selectAdminFields = "id,username,password,status,email,permissions,filters,additional_info,description"
|
selectAdminFields = "id,username,password,status,email,permissions,filters,additional_info,description"
|
||||||
|
selectAPIKeyFields = "key_id,name,api_key,scope,created_at,updated_at,last_use_at,expires_at,description,user_id,admin_id"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getSQLPlaceholders() []string {
|
func getSQLPlaceholders() []string {
|
||||||
|
@ -57,6 +58,78 @@ func getDeleteAdminQuery() string {
|
||||||
return fmt.Sprintf(`DELETE FROM %v WHERE username = %v`, sqlTableAdmins, sqlPlaceholders[0])
|
return fmt.Sprintf(`DELETE FROM %v WHERE username = %v`, sqlTableAdmins, sqlPlaceholders[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getAPIKeyByIDQuery() string {
|
||||||
|
return fmt.Sprintf(`SELECT %v FROM %v WHERE key_id = %v`, selectAPIKeyFields, sqlTableAPIKeys, sqlPlaceholders[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAPIKeysQuery(order string) string {
|
||||||
|
return fmt.Sprintf(`SELECT %v FROM %v ORDER BY key_id %v LIMIT %v OFFSET %v`, selectAPIKeyFields, sqlTableAPIKeys,
|
||||||
|
order, sqlPlaceholders[0], sqlPlaceholders[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDumpAPIKeysQuery() string {
|
||||||
|
return fmt.Sprintf(`SELECT %v FROM %v`, selectAPIKeyFields, sqlTableAPIKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAddAPIKeyQuery() string {
|
||||||
|
return fmt.Sprintf(`INSERT INTO %v (key_id,name,api_key,scope,created_at,updated_at,last_use_at,expires_at,description,user_id,admin_id)
|
||||||
|
VALUES (%v,%v,%v,%v,%v,%v,%v,%v,%v,%v,%v)`, sqlTableAPIKeys, sqlPlaceholders[0], sqlPlaceholders[1],
|
||||||
|
sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6],
|
||||||
|
sqlPlaceholders[7], sqlPlaceholders[8], sqlPlaceholders[9], sqlPlaceholders[10])
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUpdateAPIKeyQuery() string {
|
||||||
|
return fmt.Sprintf(`UPDATE %v SET name=%v,scope=%v,expires_at=%v,user_id=%v,admin_id=%v,description=%v,updated_at=%v
|
||||||
|
WHERE key_id = %v`, sqlTableAPIKeys, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2],
|
||||||
|
sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7])
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDeleteAPIKeyQuery() string {
|
||||||
|
return fmt.Sprintf(`DELETE FROM %v WHERE key_id = %v`, sqlTableAPIKeys, sqlPlaceholders[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRelatedUsersForAPIKeysQuery(apiKeys []APIKey) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
for _, k := range apiKeys {
|
||||||
|
if k.userID == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if sb.Len() == 0 {
|
||||||
|
sb.WriteString("(")
|
||||||
|
} else {
|
||||||
|
sb.WriteString(",")
|
||||||
|
}
|
||||||
|
sb.WriteString(strconv.FormatInt(k.userID, 10))
|
||||||
|
}
|
||||||
|
if sb.Len() > 0 {
|
||||||
|
sb.WriteString(")")
|
||||||
|
} else {
|
||||||
|
sb.WriteString("(0)")
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(`SELECT id,username FROM %v WHERE id IN %v`, sqlTableUsers, sb.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRelatedAdminsForAPIKeysQuery(apiKeys []APIKey) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
for _, k := range apiKeys {
|
||||||
|
if k.adminID == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if sb.Len() == 0 {
|
||||||
|
sb.WriteString("(")
|
||||||
|
} else {
|
||||||
|
sb.WriteString(",")
|
||||||
|
}
|
||||||
|
sb.WriteString(strconv.FormatInt(k.adminID, 10))
|
||||||
|
}
|
||||||
|
if sb.Len() > 0 {
|
||||||
|
sb.WriteString(")")
|
||||||
|
} else {
|
||||||
|
sb.WriteString("(0)")
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(`SELECT id,username FROM %v WHERE id IN %v`, sqlTableAdmins, sb.String())
|
||||||
|
}
|
||||||
|
|
||||||
func getUserByUsernameQuery() string {
|
func getUserByUsernameQuery() string {
|
||||||
return fmt.Sprintf(`SELECT %v FROM %v WHERE username = %v`, selectUserFields, sqlTableUsers, sqlPlaceholders[0])
|
return fmt.Sprintf(`SELECT %v FROM %v WHERE username = %v`, selectUserFields, sqlTableUsers, sqlPlaceholders[0])
|
||||||
}
|
}
|
||||||
|
@ -87,6 +160,10 @@ func getUpdateLastLoginQuery() string {
|
||||||
return fmt.Sprintf(`UPDATE %v SET last_login = %v WHERE username = %v`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1])
|
return fmt.Sprintf(`UPDATE %v SET last_login = %v WHERE username = %v`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getUpdateAPIKeyLastUseQuery() string {
|
||||||
|
return fmt.Sprintf(`UPDATE %v SET last_use_at = %v WHERE key_id = %v`, sqlTableAPIKeys, sqlPlaceholders[0], sqlPlaceholders[1])
|
||||||
|
}
|
||||||
|
|
||||||
func getQuotaQuery() string {
|
func getQuotaQuery() string {
|
||||||
return fmt.Sprintf(`SELECT used_quota_size,used_quota_files FROM %v WHERE username = %v`, sqlTableUsers,
|
return fmt.Sprintf(`SELECT used_quota_size,used_quota_files FROM %v WHERE username = %v`, sqlTableUsers,
|
||||||
sqlPlaceholders[0])
|
sqlPlaceholders[0])
|
||||||
|
|
|
@ -179,6 +179,18 @@ func (u *User) isFsEqual(other *User) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckLoginConditions checks if the user is active and not expired
|
||||||
|
func (u *User) CheckLoginConditions() error {
|
||||||
|
if u.Status < 1 {
|
||||||
|
return fmt.Errorf("user %#v is disabled", u.Username)
|
||||||
|
}
|
||||||
|
if u.ExpirationDate > 0 && u.ExpirationDate < util.GetTimeAsMsSinceEpoch(time.Now()) {
|
||||||
|
return fmt.Errorf("user %#v is expired, expiration timestamp: %v current timestamp: %v", u.Username,
|
||||||
|
u.ExpirationDate, util.GetTimeAsMsSinceEpoch(time.Now()))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// hideConfidentialData hides user confidential data
|
// hideConfidentialData hides user confidential data
|
||||||
func (u *User) hideConfidentialData() {
|
func (u *User) hideConfidentialData() {
|
||||||
u.Password = ""
|
u.Password = ""
|
||||||
|
@ -997,6 +1009,7 @@ func (u *User) getACopy() User {
|
||||||
filters.Hooks.PreLoginDisabled = u.Filters.Hooks.PreLoginDisabled
|
filters.Hooks.PreLoginDisabled = u.Filters.Hooks.PreLoginDisabled
|
||||||
filters.Hooks.CheckPasswordDisabled = u.Filters.Hooks.CheckPasswordDisabled
|
filters.Hooks.CheckPasswordDisabled = u.Filters.Hooks.CheckPasswordDisabled
|
||||||
filters.DisableFsChecks = u.Filters.DisableFsChecks
|
filters.DisableFsChecks = u.Filters.DisableFsChecks
|
||||||
|
filters.AllowAPIKeyAuth = u.Filters.AllowAPIKeyAuth
|
||||||
filters.WebClient = make([]string, len(u.Filters.WebClient))
|
filters.WebClient = make([]string, len(u.Filters.WebClient))
|
||||||
copy(filters.WebClient, u.Filters.WebClient)
|
copy(filters.WebClient, u.Filters.WebClient)
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ And then you can configure:
|
||||||
|
|
||||||
So a host is banned, for `ban_time` minutes, if the sum of the scores has exceeded the defined threshold during the last observation time minutes.
|
So a host is banned, for `ban_time` minutes, if the sum of the scores has exceeded the defined threshold during the last observation time minutes.
|
||||||
|
|
||||||
Each event type can be weighted by an integer. If `score_invalid` is 3 and `threshold` is 8, it will be banned after 3 login attempts with an invalid user within the configured `observation_time`.
|
By defining the scores, each type of event can be weighted. Let's see an example: if `score_invalid` is 3 and `threshold` is 8, a host will be banned after 3 login attempts with an non-existent user within the configured `observation_time`.
|
||||||
|
|
||||||
A banned IP has no score, it makes no sense to accumulate host events in memory for an already banned IP address.
|
A banned IP has no score, it makes no sense to accumulate host events in memory for an already banned IP address.
|
||||||
|
|
||||||
|
|
|
@ -6,11 +6,6 @@ If quota tracking is enabled in the configuration file, then the used size and n
|
||||||
|
|
||||||
REST API are protected using JSON Web Tokens (JWT) authentication and can be exposed over HTTPS. You can also configure client certificate authentication in addition to JWT.
|
REST API are protected using JSON Web Tokens (JWT) authentication and can be exposed over HTTPS. You can also configure client certificate authentication in addition to JWT.
|
||||||
|
|
||||||
The default credentials are:
|
|
||||||
|
|
||||||
- username: `admin`
|
|
||||||
- password: `password`
|
|
||||||
|
|
||||||
You can get a JWT token using the `/api/v2/token` endpoint, you need to authenticate using HTTP Basic authentication and the credentials of an active administrator. Here is a sample response:
|
You can get a JWT token using the `/api/v2/token` endpoint, you need to authenticate using HTTP Basic authentication and the credentials of an active administrator. Here is a sample response:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
@ -37,11 +32,35 @@ You can create other administrator and assign them the following permissions:
|
||||||
- view and start quota scans
|
- view and start quota scans
|
||||||
- view defender
|
- view defender
|
||||||
- manage defender
|
- manage defender
|
||||||
|
- manage API keys
|
||||||
- manage system
|
- manage system
|
||||||
- manage admins
|
- manage admins
|
||||||
|
|
||||||
You can also restrict administrator access based on the source IP address. If you are running SFTPGo behind a reverse proxy you need to allow both the proxy IP address and the real client IP.
|
You can also restrict administrator access based on the source IP address. If you are running SFTPGo behind a reverse proxy you need to allow both the proxy IP address and the real client IP.
|
||||||
|
|
||||||
|
As alternative authentication method you can use API keys. API keys are mainly designed for machine-to-machine communications and a static API key is intrinsically less secure than a short lived JWT token.
|
||||||
|
To generate API keys you first need to get a JWT token and then you can use the `/api/v2/apikeys` endpoint to manage your API keys.
|
||||||
|
|
||||||
|
The API keys allow the impersonation of users and administrators, using the API keys you inherit the permissions of the associated user/admin.
|
||||||
|
|
||||||
|
The user/admin association can be:
|
||||||
|
|
||||||
|
- static, a user/admin is explictly associated to the API key
|
||||||
|
- dynamic, the API key has no user/admin associated, you need to add ".username" at the end of the key to specificy the user/admin to impersonate. For example if your API key is `6ajKLwswLccVBGpZGv596G.ySAXc8vtp9hMiwAuaLtzof` and you want to impersonate the admin with username `myadmin` you have to use `6ajKLwswLccVBGpZGv596G.ySAXc8vtp9hMiwAuaLtzof.myadmin` as API key.
|
||||||
|
|
||||||
|
The API key scope defines if the API key can impersonate users or admins.
|
||||||
|
Before you can impersonate a user/admin you have to set `allow_api_key_auth` at user/admin level. Each user/admin can always revoke this permission.
|
||||||
|
|
||||||
|
The generated API key is returned in the response body when you create a new API key object. It is not stored as plain text, you need to save it after the initial creation, there is no way to display the API key as plain text after the initial creation.
|
||||||
|
|
||||||
|
API keys are not allowed for the following REST APIs:
|
||||||
|
|
||||||
|
- manage API keys itself. You cannot create, update, delete, enumerate API keys if you are logged in with an API key
|
||||||
|
- change password or public keys for the associated user
|
||||||
|
- update the impersonated admin
|
||||||
|
|
||||||
|
Please keep in mind that using an API key not associated with any administrator it is still possible to create a new administrator, with full permissions, and then impersonate it: be careful if you share unassociated API keys with third parties and with the `manage adminis` permission granted, they will basically allow full access, the only restriction is that the impersonated admin cannot be modified.
|
||||||
|
|
||||||
The OpenAPI 3 schema for the exposed API can be found inside the source tree: [openapi.yaml](../httpd/schema/openapi.yaml "OpenAPI 3 specs"). If you want to render the schema without importing it manually, you can explore it on [Stoplight](https://sftpgo.stoplight.io/docs/sftpgo/openapi.yaml).
|
The OpenAPI 3 schema for the exposed API can be found inside the source tree: [openapi.yaml](../httpd/schema/openapi.yaml "OpenAPI 3 specs"). If you want to render the schema without importing it manually, you can explore it on [Stoplight](https://sftpgo.stoplight.io/docs/sftpgo/openapi.yaml).
|
||||||
|
|
||||||
You can generate your own REST client in your preferred programming language, or even bash scripts, using an OpenAPI generator such as [swagger-codegen](https://github.com/swagger-api/swagger-codegen) or [OpenAPI Generator](https://openapi-generator.tech/).
|
You can generate your own REST client in your preferred programming language, or even bash scripts, using an OpenAPI generator such as [swagger-codegen](https://github.com/swagger-api/swagger-codegen) or [OpenAPI Generator](https://openapi-generator.tech/).
|
||||||
|
|
17
go.mod
17
go.mod
|
@ -7,7 +7,7 @@ require (
|
||||||
github.com/Azure/azure-storage-blob-go v0.14.0
|
github.com/Azure/azure-storage-blob-go v0.14.0
|
||||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
||||||
github.com/alexedwards/argon2id v0.0.0-20210511081203-7d35d68092b8
|
github.com/alexedwards/argon2id v0.0.0-20210511081203-7d35d68092b8
|
||||||
github.com/aws/aws-sdk-go v1.40.16
|
github.com/aws/aws-sdk-go v1.40.23
|
||||||
github.com/cockroachdb/cockroach-go/v2 v2.1.1
|
github.com/cockroachdb/cockroach-go/v2 v2.1.1
|
||||||
github.com/eikenb/pipeat v0.0.0-20210603033007-44fc3ffce52b
|
github.com/eikenb/pipeat v0.0.0-20210603033007-44fc3ffce52b
|
||||||
github.com/fatih/color v1.12.0 // indirect
|
github.com/fatih/color v1.12.0 // indirect
|
||||||
|
@ -26,12 +26,13 @@ require (
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.0
|
github.com/hashicorp/go-retryablehttp v0.7.0
|
||||||
github.com/hashicorp/yamux v0.0.0-20210707203944-259a57b3608c // indirect
|
github.com/hashicorp/yamux v0.0.0-20210707203944-259a57b3608c // indirect
|
||||||
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
|
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
|
||||||
github.com/klauspost/compress v1.13.3
|
github.com/klauspost/compress v1.13.4
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
|
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
|
||||||
github.com/lestrrat-go/jwx v1.2.5
|
github.com/lestrrat-go/jwx v1.2.5
|
||||||
github.com/lib/pq v1.10.2
|
github.com/lib/pq v1.10.2
|
||||||
|
github.com/lithammer/shortuuid/v3 v3.0.7
|
||||||
github.com/mattn/go-isatty v0.0.13 // indirect
|
github.com/mattn/go-isatty v0.0.13 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.8
|
github.com/mattn/go-sqlite3 v1.14.8
|
||||||
github.com/miekg/dns v1.1.43 // indirect
|
github.com/miekg/dns v1.1.43 // indirect
|
||||||
|
@ -59,12 +60,12 @@ require (
|
||||||
go.uber.org/automaxprocs v1.4.0
|
go.uber.org/automaxprocs v1.4.0
|
||||||
gocloud.dev v0.23.0
|
gocloud.dev v0.23.0
|
||||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
|
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
|
||||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e
|
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
|
golang.org/x/sys v0.0.0-20210817134402-fefb4affbef3
|
||||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac
|
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac
|
||||||
google.golang.org/api v0.52.0
|
google.golang.org/api v0.54.0
|
||||||
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67 // indirect
|
google.golang.org/genproto v0.0.0-20210816143620-e15ff196659d // indirect
|
||||||
google.golang.org/grpc v1.39.1
|
google.golang.org/grpc v1.40.0
|
||||||
google.golang.org/protobuf v1.27.1
|
google.golang.org/protobuf v1.27.1
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||||
)
|
)
|
||||||
|
@ -74,5 +75,5 @@ replace (
|
||||||
github.com/fclairamb/ftpserverlib => github.com/drakkan/ftpserverlib v0.0.0-20210805132427-425f32d9dc15
|
github.com/fclairamb/ftpserverlib => github.com/drakkan/ftpserverlib v0.0.0-20210805132427-425f32d9dc15
|
||||||
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
|
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
|
||||||
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20210515063737-edf1d3b63536
|
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20210515063737-edf1d3b63536
|
||||||
golang.org/x/net => github.com/drakkan/net v0.0.0-20210725074420-30b60d4a1e60
|
golang.org/x/net => github.com/drakkan/net v0.0.0-20210817141953-39359926843c
|
||||||
)
|
)
|
||||||
|
|
49
go.sum
49
go.sum
|
@ -23,8 +23,9 @@ cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb
|
||||||
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
|
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
|
||||||
cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
|
cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
|
||||||
cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
|
cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
|
||||||
cloud.google.com/go v0.88.0 h1:MZ2cf9Elnv1wqccq8ooKO2MqHQLc+ChCp/+QWObCpxg=
|
cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
|
||||||
cloud.google.com/go v0.88.0/go.mod h1:dnKwfYbP9hQhefiUvpbcAyoGSHUrOxR20JVElLiUvEY=
|
cloud.google.com/go v0.90.0 h1:MjvSkUq8RuAb+2JLDi5VQmmExRJPUQ3JLCWpRB6fmdw=
|
||||||
|
cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
|
||||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||||
|
@ -96,6 +97,7 @@ github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaa
|
||||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo=
|
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo=
|
||||||
github.com/GoogleCloudPlatform/cloudsql-proxy v1.22.0/go.mod h1:mAm5O/zik2RFmcpigNjg6nMotDL8ZXJaxKzgGVcSMFA=
|
github.com/GoogleCloudPlatform/cloudsql-proxy v1.22.0/go.mod h1:mAm5O/zik2RFmcpigNjg6nMotDL8ZXJaxKzgGVcSMFA=
|
||||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
|
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
|
||||||
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||||
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
|
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
|
||||||
|
@ -118,8 +120,8 @@ github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZo
|
||||||
github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||||
github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||||
github.com/aws/aws-sdk-go v1.38.68/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
github.com/aws/aws-sdk-go v1.38.68/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||||
github.com/aws/aws-sdk-go v1.40.16 h1:Tgg7i9ee2j6ir2EfejPDJBB3PyfUM4dPlvmMLtvJVfo=
|
github.com/aws/aws-sdk-go v1.40.23 h1:o0cw9jVlbXkDd3eZU7MDIXp3a8zhECnl0BfUs2WNBIM=
|
||||||
github.com/aws/aws-sdk-go v1.40.16/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
|
github.com/aws/aws-sdk-go v1.40.23/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.7.0/go.mod h1:tb9wi5s61kTDA5qCkcDbt3KRVV74GGslQkl/DRdX/P4=
|
github.com/aws/aws-sdk-go-v2 v1.7.0/go.mod h1:tb9wi5s61kTDA5qCkcDbt3KRVV74GGslQkl/DRdX/P4=
|
||||||
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.5.0/go.mod h1:acH3+MQoiMzozT/ivU+DbRg7Ooo2298RdRaWcOv+4vM=
|
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.5.0/go.mod h1:acH3+MQoiMzozT/ivU+DbRg7Ooo2298RdRaWcOv+4vM=
|
||||||
github.com/aws/smithy-go v1.5.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
|
github.com/aws/smithy-go v1.5.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
|
||||||
|
@ -134,6 +136,8 @@ github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEe
|
||||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
|
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||||
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
|
@ -178,8 +182,8 @@ github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 h1:LPH1dEblAOO/LoG7yHP
|
||||||
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU=
|
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU=
|
||||||
github.com/drakkan/ftpserverlib v0.0.0-20210805132427-425f32d9dc15 h1:J7FZPDILyOMYtShuM5hH3GLTL1cCDtoJ1InsxEyl798=
|
github.com/drakkan/ftpserverlib v0.0.0-20210805132427-425f32d9dc15 h1:J7FZPDILyOMYtShuM5hH3GLTL1cCDtoJ1InsxEyl798=
|
||||||
github.com/drakkan/ftpserverlib v0.0.0-20210805132427-425f32d9dc15/go.mod h1:+Doq95UijHTIaJcWREhyu9dyQOqyoULbVU3OXgs8wEI=
|
github.com/drakkan/ftpserverlib v0.0.0-20210805132427-425f32d9dc15/go.mod h1:+Doq95UijHTIaJcWREhyu9dyQOqyoULbVU3OXgs8wEI=
|
||||||
github.com/drakkan/net v0.0.0-20210725074420-30b60d4a1e60 h1:qGPbhCgKiOglRLoNPgAwdTOp3xW3TC96RSGLkmVdsd0=
|
github.com/drakkan/net v0.0.0-20210817141953-39359926843c h1:mK9lUujDLUgprG2ORSgea+EYzH6fb8SFzHAzlm4pUCk=
|
||||||
github.com/drakkan/net v0.0.0-20210725074420-30b60d4a1e60/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
github.com/drakkan/net v0.0.0-20210817141953-39359926843c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
github.com/drakkan/pipeat v0.0.0-20210805162858-70e57fa8a639 h1:8tfGdb4kg/YCvAbIrsMazgoNtnqdOqQVDKW12uUCuuU=
|
github.com/drakkan/pipeat v0.0.0-20210805162858-70e57fa8a639 h1:8tfGdb4kg/YCvAbIrsMazgoNtnqdOqQVDKW12uUCuuU=
|
||||||
github.com/drakkan/pipeat v0.0.0-20210805162858-70e57fa8a639/go.mod h1:kltMsfRMTHSFdMbK66XdS8mfMW77+FZA1fGY1xYMF84=
|
github.com/drakkan/pipeat v0.0.0-20210805162858-70e57fa8a639/go.mod h1:kltMsfRMTHSFdMbK66XdS8mfMW77+FZA1fGY1xYMF84=
|
||||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
|
@ -331,7 +335,8 @@ github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLe
|
||||||
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
github.com/google/pprof v0.0.0-20210715191844-86eeefc3e471/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
|
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||||
|
@ -474,8 +479,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
||||||
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||||
github.com/klauspost/compress v1.11.12/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
github.com/klauspost/compress v1.11.12/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||||
github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||||
github.com/klauspost/compress v1.13.3 h1:BtAvtV1+h0YwSVwWoYXMREPpYu9VzTJ9QDI1TEg/iQQ=
|
github.com/klauspost/compress v1.13.4 h1:0zhec2I8zGnjWcKyLl6i3gPqKANCCn5e9xmviEEeX6s=
|
||||||
github.com/klauspost/compress v1.13.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
@ -522,6 +527,8 @@ github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
|
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
|
||||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=
|
||||||
|
github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=
|
||||||
github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
|
github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
|
||||||
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
|
@ -678,6 +685,7 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1
|
||||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||||
|
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
|
github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
|
||||||
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
||||||
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
||||||
|
@ -808,8 +816,9 @@ golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78/go.mod h1:KelEdhl1UZF7XfJ
|
||||||
golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
golang.org/x/oauth2 v0.0.0-20210615190721-d04028783cf1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20210615190721-d04028783cf1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914 h1:3B43BWw0xEBsLZ/NO1VALz6fppU3481pik+2Ksv45z8=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a h1:4Kd8OPUx1xgUwrHDaviWZO8MsgoZTZYC3g+8m16RBww=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
@ -894,8 +903,10 @@ golang.org/x/sys v0.0.0-20210601080250-7ecdf8ef093b/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
|
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210817134402-fefb4affbef3 h1:VU4cjb3pNZovHYRs/2E8zPrpLC05+GT2uqqgIFtt0jc=
|
||||||
|
golang.org/x/sys v0.0.0-20210817134402-fefb4affbef3/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
@ -1018,8 +1029,9 @@ google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59t
|
||||||
google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
|
google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
|
||||||
google.golang.org/api v0.49.0/go.mod h1:BECiH72wsfwUvOVn3+btPD5WHi0LzavZReBndi42L18=
|
google.golang.org/api v0.49.0/go.mod h1:BECiH72wsfwUvOVn3+btPD5WHi0LzavZReBndi42L18=
|
||||||
google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
|
google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
|
||||||
google.golang.org/api v0.52.0 h1:m5FLEd6dp5CU1F0tMWyqDi2XjchviIz8ntzOSz7w8As=
|
google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
|
||||||
google.golang.org/api v0.52.0/go.mod h1:Him/adpjt0sxtkWViy0b6xyKW/SD71CwdJ7HqJo7SrU=
|
google.golang.org/api v0.54.0 h1:ECJUVngj71QI6XEm7b1sAf8BljU5inEhMbKPR8Lxhhk=
|
||||||
|
google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
@ -1085,10 +1097,12 @@ google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxH
|
||||||
google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
|
google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
|
||||||
google.golang.org/genproto v0.0.0-20210624174822-c5cf32407d0a/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
|
google.golang.org/genproto v0.0.0-20210624174822-c5cf32407d0a/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
|
||||||
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
|
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
|
||||||
google.golang.org/genproto v0.0.0-20210721163202-f1cecdd8b78a/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
|
google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
|
||||||
google.golang.org/genproto v0.0.0-20210722135532-667f2b7c528f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
|
google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
|
||||||
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67 h1:VmMSf20ssFK0+u1dscyTH9bU4/M4y+X/xNfkvD6kGtM=
|
google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
|
||||||
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
|
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
|
||||||
|
google.golang.org/genproto v0.0.0-20210816143620-e15ff196659d h1:fPtHPeysWvGVJwQFKu3B7H2DB2sOEsW7UTayKkWESKw=
|
||||||
|
google.golang.org/genproto v0.0.0-20210816143620-e15ff196659d/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
||||||
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
|
@ -1115,8 +1129,9 @@ google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ
|
||||||
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||||
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||||
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
|
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
|
||||||
google.golang.org/grpc v1.39.1 h1:f37vZbBVTiJ6jKG5mWz8ySOBxNqy6ViPgyhSdVnxF3E=
|
|
||||||
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
|
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
|
||||||
|
google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q=
|
||||||
|
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func getAdmins(w http.ResponseWriter, r *http.Request) {
|
func getAdmins(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
limit, offset, order, err := getSearchFilters(w, r)
|
limit, offset, order, err := getSearchFilters(w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
@ -27,6 +28,7 @@ func getAdmins(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAdminByUsername(w http.ResponseWriter, r *http.Request) {
|
func getAdminByUsername(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
username := getURLParam(r, "username")
|
username := getURLParam(r, "username")
|
||||||
renderAdmin(w, r, username, http.StatusOK)
|
renderAdmin(w, r, username, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
@ -84,6 +86,11 @@ func updateAdmin(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if username == claims.Username {
|
if username == claims.Username {
|
||||||
|
if claims.APIKeyID != "" {
|
||||||
|
sendAPIResponse(w, r, errors.New("updating the admin impersonated with an API key is not allowed"), "",
|
||||||
|
http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
if claims.isCriticalPermRemoved(admin.Permissions) {
|
if claims.isCriticalPermRemoved(admin.Permissions) {
|
||||||
sendAPIResponse(w, r, errors.New("you cannot remove these permissions to yourself"), "", http.StatusBadRequest)
|
sendAPIResponse(w, r, errors.New("you cannot remove these permissions to yourself"), "", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
@ -103,6 +110,7 @@ func updateAdmin(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteAdmin(w http.ResponseWriter, r *http.Request) {
|
func deleteAdmin(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
username := getURLParam(r, "username")
|
username := getURLParam(r, "username")
|
||||||
claims, err := getTokenClaims(r)
|
claims, err := getTokenClaims(r)
|
||||||
if err != nil || claims.Username == "" {
|
if err != nil || claims.Username == "" {
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func getDefenderHosts(w http.ResponseWriter, r *http.Request) {
|
func getDefenderHosts(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
hosts := common.GetDefenderHosts()
|
hosts := common.GetDefenderHosts()
|
||||||
if hosts == nil {
|
if hosts == nil {
|
||||||
render.JSON(w, r, make([]common.DefenderEntry, 0))
|
render.JSON(w, r, make([]common.DefenderEntry, 0))
|
||||||
|
@ -23,6 +24,7 @@ func getDefenderHosts(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDefenderHostByID(w http.ResponseWriter, r *http.Request) {
|
func getDefenderHostByID(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
ip, err := getIPFromID(r)
|
ip, err := getIPFromID(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||||
|
@ -37,6 +39,7 @@ func getDefenderHostByID(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteDefenderHostByID(w http.ResponseWriter, r *http.Request) {
|
func deleteDefenderHostByID(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
ip, err := getIPFromID(r)
|
ip, err := getIPFromID(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||||
|
@ -51,6 +54,7 @@ func deleteDefenderHostByID(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getBanTime(w http.ResponseWriter, r *http.Request) {
|
func getBanTime(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
ip := r.URL.Query().Get("ip")
|
ip := r.URL.Query().Get("ip")
|
||||||
err := validateIPAddress(ip)
|
err := validateIPAddress(ip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -72,6 +76,7 @@ func getBanTime(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getScore(w http.ResponseWriter, r *http.Request) {
|
func getScore(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
ip := r.URL.Query().Get("ip")
|
ip := r.URL.Query().Get("ip")
|
||||||
err := validateIPAddress(ip)
|
err := validateIPAddress(ip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func getFolders(w http.ResponseWriter, r *http.Request) {
|
func getFolders(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
limit, offset, order, err := getSearchFilters(w, r)
|
limit, offset, order, err := getSearchFilters(w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
@ -99,11 +100,13 @@ func renderFolder(w http.ResponseWriter, r *http.Request, name string, status in
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFolderByName(w http.ResponseWriter, r *http.Request) {
|
func getFolderByName(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
name := getURLParam(r, "name")
|
name := getURLParam(r, "name")
|
||||||
renderFolder(w, r, name, http.StatusOK)
|
renderFolder(w, r, name, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteFolder(w http.ResponseWriter, r *http.Request) {
|
func deleteFolder(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
name := getURLParam(r, "name")
|
name := getURLParam(r, "name")
|
||||||
err := dataprovider.DeleteFolder(name)
|
err := dataprovider.DeleteFolder(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -45,6 +45,7 @@ func getUserConnection(w http.ResponseWriter, r *http.Request) (*Connection, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func readUserFolder(w http.ResponseWriter, r *http.Request) {
|
func readUserFolder(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
connection, err := getUserConnection(w, r)
|
connection, err := getUserConnection(w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
@ -130,6 +131,7 @@ func deleteUserDir(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUserFile(w http.ResponseWriter, r *http.Request) {
|
func getUserFile(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
connection, err := getUserConnection(w, r)
|
connection, err := getUserConnection(w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
@ -278,6 +280,7 @@ func deleteUserFile(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUserFilesAsZipStream(w http.ResponseWriter, r *http.Request) {
|
func getUserFilesAsZipStream(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
connection, err := getUserConnection(w, r)
|
connection, err := getUserConnection(w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
@ -302,6 +305,7 @@ func getUserFilesAsZipStream(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUserPublicKeys(w http.ResponseWriter, r *http.Request) {
|
func getUserPublicKeys(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
claims, err := getTokenClaims(r)
|
claims, err := getTokenClaims(r)
|
||||||
if err != nil || claims.Username == "" {
|
if err != nil || claims.Username == "" {
|
||||||
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||||
|
|
99
httpd/api_keys.go
Normal file
99
httpd/api_keys.go
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
package httpd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-chi/render"
|
||||||
|
|
||||||
|
"github.com/drakkan/sftpgo/v2/dataprovider"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getAPIKeys(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
limit, offset, order, err := getSearchFilters(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiKeys, err := dataprovider.GetAPIKeys(limit, offset, order)
|
||||||
|
if err != nil {
|
||||||
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
render.JSON(w, r, apiKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAPIKeyByID(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
keyID := getURLParam(r, "id")
|
||||||
|
apiKey, err := dataprovider.APIKeyExists(keyID)
|
||||||
|
if err != nil {
|
||||||
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
apiKey.HideConfidentialData()
|
||||||
|
|
||||||
|
render.JSON(w, r, apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
var apiKey dataprovider.APIKey
|
||||||
|
err := render.DecodeJSON(r.Body, &apiKey)
|
||||||
|
if err != nil {
|
||||||
|
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
apiKey.ID = 0
|
||||||
|
apiKey.KeyID = ""
|
||||||
|
apiKey.Key = ""
|
||||||
|
err = dataprovider.AddAPIKey(&apiKey)
|
||||||
|
if err != nil {
|
||||||
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response := make(map[string]string)
|
||||||
|
response["message"] = "API key created. This is the only time the API key is visible, please save it."
|
||||||
|
response["key"] = apiKey.DisplayKey()
|
||||||
|
w.Header().Add("Location", fmt.Sprintf("%v/%v", apiKeysPath, apiKey.KeyID))
|
||||||
|
w.Header().Add("X-Object-ID", apiKey.KeyID)
|
||||||
|
ctx := context.WithValue(r.Context(), render.StatusCtxKey, http.StatusCreated)
|
||||||
|
render.JSON(w, r.WithContext(ctx), response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
keyID := getURLParam(r, "id")
|
||||||
|
apiKey, err := dataprovider.APIKeyExists(keyID)
|
||||||
|
if err != nil {
|
||||||
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = render.DecodeJSON(r.Body, &apiKey)
|
||||||
|
if err != nil {
|
||||||
|
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiKey.KeyID = keyID
|
||||||
|
if err := dataprovider.UpdateAPIKey(&apiKey); err != nil {
|
||||||
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sendAPIResponse(w, r, nil, "API key updated", http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
keyID := getURLParam(r, "id")
|
||||||
|
|
||||||
|
err := dataprovider.DeleteAPIKey(keyID)
|
||||||
|
if err != nil {
|
||||||
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sendAPIResponse(w, r, err, "API key deleted", http.StatusOK)
|
||||||
|
}
|
|
@ -35,6 +35,7 @@ func validateBackupFile(outputFile string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func dumpData(w http.ResponseWriter, r *http.Request) {
|
func dumpData(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
var outputFile, outputData, indent string
|
var outputFile, outputData, indent string
|
||||||
if _, ok := r.URL.Query()["output-file"]; ok {
|
if _, ok := r.URL.Query()["output-file"]; ok {
|
||||||
outputFile = strings.TrimSpace(r.URL.Query().Get("output-file"))
|
outputFile = strings.TrimSpace(r.URL.Query().Get("output-file"))
|
||||||
|
@ -117,6 +118,7 @@ func loadDataFromRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadData(w http.ResponseWriter, r *http.Request) {
|
func loadData(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
inputFile, scanQuota, mode, err := getLoaddataOptions(r)
|
inputFile, scanQuota, mode, err := getLoaddataOptions(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||||
|
@ -166,6 +168,10 @@ func restoreBackup(content []byte, inputFile string, scanQuota, mode int) error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = RestoreAPIKeys(dump.APIKeys, inputFile, mode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
logger.Debug(logSender, "", "backup restored, users: %v, folders: %v, admins: %vs",
|
logger.Debug(logSender, "", "backup restored, users: %v, folders: %v, admins: %vs",
|
||||||
len(dump.Users), len(dump.Folders), len(dump.Admins))
|
len(dump.Users), len(dump.Folders), len(dump.Admins))
|
||||||
|
|
||||||
|
@ -216,7 +222,7 @@ func RestoreFolders(folders []vfs.BaseVirtualFolder, inputFile string, mode, sca
|
||||||
logger.Debug(logSender, "", "adding new folder: %+v, dump file: %#v, error: %v", folder, inputFile, err)
|
logger.Debug(logSender, "", "adding new folder: %+v, dump file: %#v, error: %v", folder, inputFile, err)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("unable to restore folder %#v: %w", folder.Name, err)
|
||||||
}
|
}
|
||||||
if scanQuota >= 1 {
|
if scanQuota >= 1 {
|
||||||
if common.QuotaScans.AddVFolderQuotaScan(folder.Name) {
|
if common.QuotaScans.AddVFolderQuotaScan(folder.Name) {
|
||||||
|
@ -228,6 +234,36 @@ func RestoreFolders(folders []vfs.BaseVirtualFolder, inputFile string, mode, sca
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RestoreAPIKeys restores the specified API keys
|
||||||
|
func RestoreAPIKeys(apiKeys []dataprovider.APIKey, inputFile string, mode int) error {
|
||||||
|
for _, apiKey := range apiKeys {
|
||||||
|
apiKey := apiKey // pin
|
||||||
|
if apiKey.Key == "" {
|
||||||
|
logger.Warn(logSender, "", "cannot restore empty API key")
|
||||||
|
return fmt.Errorf("cannot restore an empty API key: %+v", apiKey)
|
||||||
|
}
|
||||||
|
k, err := dataprovider.APIKeyExists(apiKey.KeyID)
|
||||||
|
if err == nil {
|
||||||
|
if mode == 1 {
|
||||||
|
logger.Debug(logSender, "", "loaddata mode 1, existing API key %#v not updated", apiKey.KeyID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
apiKey.ID = k.ID
|
||||||
|
err = dataprovider.UpdateAPIKey(&apiKey)
|
||||||
|
apiKey.Key = redactedSecret
|
||||||
|
logger.Debug(logSender, "", "restoring existing API key: %+v, dump file: %#v, error: %v", apiKey, inputFile, err)
|
||||||
|
} else {
|
||||||
|
err = dataprovider.AddAPIKey(&apiKey)
|
||||||
|
apiKey.Key = redactedSecret
|
||||||
|
logger.Debug(logSender, "", "adding new API key: %+v, dump file: %#v, error: %v", apiKey, inputFile, err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to restore API key %#v: %w", apiKey.KeyID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// RestoreAdmins restores the specified admins
|
// RestoreAdmins restores the specified admins
|
||||||
func RestoreAdmins(admins []dataprovider.Admin, inputFile string, mode int) error {
|
func RestoreAdmins(admins []dataprovider.Admin, inputFile string, mode int) error {
|
||||||
for _, admin := range admins {
|
for _, admin := range admins {
|
||||||
|
@ -248,7 +284,7 @@ func RestoreAdmins(admins []dataprovider.Admin, inputFile string, mode int) erro
|
||||||
logger.Debug(logSender, "", "adding new admin: %+v, dump file: %#v, error: %v", admin, inputFile, err)
|
logger.Debug(logSender, "", "adding new admin: %+v, dump file: %#v, error: %v", admin, inputFile, err)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("unable to restore admin %#v: %w", admin.Username, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -278,7 +314,7 @@ func RestoreUsers(users []dataprovider.User, inputFile string, mode, scanQuota i
|
||||||
logger.Debug(logSender, "", "adding new user: %+v, dump file: %#v, error: %v", user, inputFile, err)
|
logger.Debug(logSender, "", "adding new user: %+v, dump file: %#v, error: %v", user, inputFile, err)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("unable to restoreuser %#v: %w", user.Username, err)
|
||||||
}
|
}
|
||||||
if scanQuota == 1 || (scanQuota == 2 && user.HasQuotaRestrictions()) {
|
if scanQuota == 1 || (scanQuota == 2 && user.HasQuotaRestrictions()) {
|
||||||
if common.QuotaScans.AddUserQuotaScan(user.Username) {
|
if common.QuotaScans.AddUserQuotaScan(user.Username) {
|
||||||
|
|
|
@ -23,10 +23,12 @@ type quotaUsage struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUsersQuotaScans(w http.ResponseWriter, r *http.Request) {
|
func getUsersQuotaScans(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
render.JSON(w, r, common.QuotaScans.GetUsersQuotaScans())
|
render.JSON(w, r, common.QuotaScans.GetUsersQuotaScans())
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFoldersQuotaScans(w http.ResponseWriter, r *http.Request) {
|
func getFoldersQuotaScans(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
render.JSON(w, r, common.QuotaScans.GetVFoldersQuotaScans())
|
render.JSON(w, r, common.QuotaScans.GetVFoldersQuotaScans())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func getUsers(w http.ResponseWriter, r *http.Request) {
|
func getUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
limit, offset, order, err := getSearchFilters(w, r)
|
limit, offset, order, err := getSearchFilters(w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
@ -31,6 +32,7 @@ func getUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUserByUsername(w http.ResponseWriter, r *http.Request) {
|
func getUserByUsername(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
username := getURLParam(r, "username")
|
username := getURLParam(r, "username")
|
||||||
renderUser(w, r, username, http.StatusOK)
|
renderUser(w, r, username, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
@ -164,6 +166,7 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteUser(w http.ResponseWriter, r *http.Request) {
|
func deleteUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
username := getURLParam(r, "username")
|
username := getURLParam(r, "username")
|
||||||
err := dataprovider.DeleteUser(username)
|
err := dataprovider.DeleteUser(username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -30,7 +30,9 @@ type pwdChange struct {
|
||||||
|
|
||||||
func sendAPIResponse(w http.ResponseWriter, r *http.Request, err error, message string, code int) {
|
func sendAPIResponse(w http.ResponseWriter, r *http.Request, err error, message string, code int) {
|
||||||
var errorString string
|
var errorString string
|
||||||
if err != nil {
|
if _, ok := err.(*util.RecordNotFoundError); ok {
|
||||||
|
errorString = http.StatusText(http.StatusNotFound)
|
||||||
|
} else if err != nil {
|
||||||
errorString = err.Error()
|
errorString = err.Error()
|
||||||
}
|
}
|
||||||
resp := apiResponse{
|
resp := apiResponse{
|
||||||
|
@ -71,6 +73,7 @@ func getMappedStatusCode(err error) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCloseConnection(w http.ResponseWriter, r *http.Request) {
|
func handleCloseConnection(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
connectionID := getURLParam(r, "connectionID")
|
connectionID := getURLParam(r, "connectionID")
|
||||||
if connectionID == "" {
|
if connectionID == "" {
|
||||||
sendAPIResponse(w, r, nil, "connectionID is mandatory", http.StatusBadRequest)
|
sendAPIResponse(w, r, nil, "connectionID is mandatory", http.StatusBadRequest)
|
||||||
|
|
|
@ -28,6 +28,7 @@ const (
|
||||||
const (
|
const (
|
||||||
claimUsernameKey = "username"
|
claimUsernameKey = "username"
|
||||||
claimPermissionsKey = "permissions"
|
claimPermissionsKey = "permissions"
|
||||||
|
claimAPIKey = "api_key"
|
||||||
basicRealm = "Basic realm=\"SFTPGo\""
|
basicRealm = "Basic realm=\"SFTPGo\""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -43,6 +44,7 @@ type jwtTokenClaims struct {
|
||||||
Username string
|
Username string
|
||||||
Permissions []string
|
Permissions []string
|
||||||
Signature string
|
Signature string
|
||||||
|
APIKeyID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *jwtTokenClaims) asMap() map[string]interface{} {
|
func (c *jwtTokenClaims) asMap() map[string]interface{} {
|
||||||
|
@ -50,6 +52,9 @@ func (c *jwtTokenClaims) asMap() map[string]interface{} {
|
||||||
|
|
||||||
claims[claimUsernameKey] = c.Username
|
claims[claimUsernameKey] = c.Username
|
||||||
claims[claimPermissionsKey] = c.Permissions
|
claims[claimPermissionsKey] = c.Permissions
|
||||||
|
if c.APIKeyID != "" {
|
||||||
|
claims[claimAPIKey] = c.APIKeyID
|
||||||
|
}
|
||||||
claims[jwt.SubjectKey] = c.Signature
|
claims[jwt.SubjectKey] = c.Signature
|
||||||
|
|
||||||
return claims
|
return claims
|
||||||
|
@ -70,6 +75,13 @@ func (c *jwtTokenClaims) Decode(token map[string]interface{}) {
|
||||||
c.Signature = v
|
c.Signature = v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if val, ok := token[claimAPIKey]; ok {
|
||||||
|
switch v := val.(type) {
|
||||||
|
case string:
|
||||||
|
c.APIKeyID = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
permissions := token[claimPermissionsKey]
|
permissions := token[claimPermissionsKey]
|
||||||
switch v := permissions.(type) {
|
switch v := permissions.(type) {
|
||||||
case []interface{}:
|
case []interface{}:
|
||||||
|
|
224
httpd/httpd.go
224
httpd/httpd.go
|
@ -30,121 +30,128 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
logSender = "httpd"
|
logSender = "httpd"
|
||||||
tokenPath = "/api/v2/token"
|
tokenPath = "/api/v2/token"
|
||||||
logoutPath = "/api/v2/logout"
|
logoutPath = "/api/v2/logout"
|
||||||
userTokenPath = "/api/v2/user/token"
|
userTokenPath = "/api/v2/user/token"
|
||||||
userLogoutPath = "/api/v2/user/logout"
|
userLogoutPath = "/api/v2/user/logout"
|
||||||
activeConnectionsPath = "/api/v2/connections"
|
activeConnectionsPath = "/api/v2/connections"
|
||||||
quotasBasePath = "/api/v2/quotas"
|
quotasBasePath = "/api/v2/quotas"
|
||||||
quotaScanPath = "/api/v2/quota-scans"
|
quotaScanPath = "/api/v2/quota-scans"
|
||||||
quotaScanVFolderPath = "/api/v2/folder-quota-scans"
|
quotaScanVFolderPath = "/api/v2/folder-quota-scans"
|
||||||
userPath = "/api/v2/users"
|
userPath = "/api/v2/users"
|
||||||
versionPath = "/api/v2/version"
|
versionPath = "/api/v2/version"
|
||||||
folderPath = "/api/v2/folders"
|
folderPath = "/api/v2/folders"
|
||||||
serverStatusPath = "/api/v2/status"
|
serverStatusPath = "/api/v2/status"
|
||||||
dumpDataPath = "/api/v2/dumpdata"
|
dumpDataPath = "/api/v2/dumpdata"
|
||||||
loadDataPath = "/api/v2/loaddata"
|
loadDataPath = "/api/v2/loaddata"
|
||||||
updateUsedQuotaPath = "/api/v2/quota-update"
|
updateUsedQuotaPath = "/api/v2/quota-update"
|
||||||
updateFolderUsedQuotaPath = "/api/v2/folder-quota-update"
|
updateFolderUsedQuotaPath = "/api/v2/folder-quota-update"
|
||||||
defenderHosts = "/api/v2/defender/hosts"
|
defenderHosts = "/api/v2/defender/hosts"
|
||||||
defenderBanTime = "/api/v2/defender/bantime"
|
defenderBanTime = "/api/v2/defender/bantime"
|
||||||
defenderUnban = "/api/v2/defender/unban"
|
defenderUnban = "/api/v2/defender/unban"
|
||||||
defenderScore = "/api/v2/defender/score"
|
defenderScore = "/api/v2/defender/score"
|
||||||
adminPath = "/api/v2/admins"
|
adminPath = "/api/v2/admins"
|
||||||
adminPwdPath = "/api/v2/admin/changepwd"
|
adminPwdPath = "/api/v2/admin/changepwd"
|
||||||
adminPwdCompatPath = "/api/v2/changepwd/admin"
|
adminPwdCompatPath = "/api/v2/changepwd/admin"
|
||||||
userPwdPath = "/api/v2/user/changepwd"
|
userPwdPath = "/api/v2/user/changepwd"
|
||||||
userPublicKeysPath = "/api/v2/user/publickeys"
|
userPublicKeysPath = "/api/v2/user/publickeys"
|
||||||
userFolderPath = "/api/v2/user/folder"
|
userFolderPath = "/api/v2/user/folder"
|
||||||
userDirsPath = "/api/v2/user/dirs"
|
userDirsPath = "/api/v2/user/dirs"
|
||||||
userFilePath = "/api/v2/user/file"
|
userFilePath = "/api/v2/user/file"
|
||||||
userFilesPath = "/api/v2/user/files"
|
userFilesPath = "/api/v2/user/files"
|
||||||
userStreamZipPath = "/api/v2/user/streamzip"
|
userStreamZipPath = "/api/v2/user/streamzip"
|
||||||
healthzPath = "/healthz"
|
apiKeysPath = "/api/v2/apikeys"
|
||||||
webRootPathDefault = "/"
|
healthzPath = "/healthz"
|
||||||
webBasePathDefault = "/web"
|
webRootPathDefault = "/"
|
||||||
webBasePathAdminDefault = "/web/admin"
|
webBasePathDefault = "/web"
|
||||||
webBasePathClientDefault = "/web/client"
|
webBasePathAdminDefault = "/web/admin"
|
||||||
webAdminSetupPathDefault = "/web/admin/setup"
|
webBasePathClientDefault = "/web/client"
|
||||||
webLoginPathDefault = "/web/admin/login"
|
webAdminSetupPathDefault = "/web/admin/setup"
|
||||||
webLogoutPathDefault = "/web/admin/logout"
|
webLoginPathDefault = "/web/admin/login"
|
||||||
webUsersPathDefault = "/web/admin/users"
|
webLogoutPathDefault = "/web/admin/logout"
|
||||||
webUserPathDefault = "/web/admin/user"
|
webUsersPathDefault = "/web/admin/users"
|
||||||
webConnectionsPathDefault = "/web/admin/connections"
|
webUserPathDefault = "/web/admin/user"
|
||||||
webFoldersPathDefault = "/web/admin/folders"
|
webConnectionsPathDefault = "/web/admin/connections"
|
||||||
webFolderPathDefault = "/web/admin/folder"
|
webFoldersPathDefault = "/web/admin/folders"
|
||||||
webStatusPathDefault = "/web/admin/status"
|
webFolderPathDefault = "/web/admin/folder"
|
||||||
webAdminsPathDefault = "/web/admin/managers"
|
webStatusPathDefault = "/web/admin/status"
|
||||||
webAdminPathDefault = "/web/admin/manager"
|
webAdminsPathDefault = "/web/admin/managers"
|
||||||
webMaintenancePathDefault = "/web/admin/maintenance"
|
webAdminPathDefault = "/web/admin/manager"
|
||||||
webBackupPathDefault = "/web/admin/backup"
|
webMaintenancePathDefault = "/web/admin/maintenance"
|
||||||
webRestorePathDefault = "/web/admin/restore"
|
webBackupPathDefault = "/web/admin/backup"
|
||||||
webScanVFolderPathDefault = "/web/admin/quotas/scanfolder"
|
webRestorePathDefault = "/web/admin/restore"
|
||||||
webQuotaScanPathDefault = "/web/admin/quotas/scanuser"
|
webScanVFolderPathDefault = "/web/admin/quotas/scanfolder"
|
||||||
webChangeAdminPwdPathDefault = "/web/admin/changepwd"
|
webQuotaScanPathDefault = "/web/admin/quotas/scanuser"
|
||||||
webTemplateUserDefault = "/web/admin/template/user"
|
webChangeAdminPwdPathDefault = "/web/admin/changepwd"
|
||||||
webTemplateFolderDefault = "/web/admin/template/folder"
|
webAdminCredentialsPathDefault = "/web/admin/credentials"
|
||||||
webDefenderPathDefault = "/web/admin/defender"
|
webChangeAdminAPIKeyAccessPathDefault = "/web/admin/apikeyaccess"
|
||||||
webDefenderHostsPathDefault = "/web/admin/defender/hosts"
|
webTemplateUserDefault = "/web/admin/template/user"
|
||||||
webClientLoginPathDefault = "/web/client/login"
|
webTemplateFolderDefault = "/web/admin/template/folder"
|
||||||
webClientFilesPathDefault = "/web/client/files"
|
webDefenderPathDefault = "/web/admin/defender"
|
||||||
webClientDirsPathDefault = "/web/client/dirs"
|
webDefenderHostsPathDefault = "/web/admin/defender/hosts"
|
||||||
webClientDownloadZipPathDefault = "/web/client/downloadzip"
|
webClientLoginPathDefault = "/web/client/login"
|
||||||
webClientCredentialsPathDefault = "/web/client/credentials"
|
webClientFilesPathDefault = "/web/client/files"
|
||||||
webChangeClientPwdPathDefault = "/web/client/changepwd"
|
webClientDirsPathDefault = "/web/client/dirs"
|
||||||
webChangeClientKeysPathDefault = "/web/client/managekeys"
|
webClientDownloadZipPathDefault = "/web/client/downloadzip"
|
||||||
webClientLogoutPathDefault = "/web/client/logout"
|
webClientCredentialsPathDefault = "/web/client/credentials"
|
||||||
webStaticFilesPathDefault = "/static"
|
webChangeClientPwdPathDefault = "/web/client/changepwd"
|
||||||
|
webChangeClientKeysPathDefault = "/web/client/managekeys"
|
||||||
|
webChangeClientAPIKeyAccessPathDefault = "/web/client/apikeyaccess"
|
||||||
|
webClientLogoutPathDefault = "/web/client/logout"
|
||||||
|
webStaticFilesPathDefault = "/static"
|
||||||
// MaxRestoreSize defines the max size for the loaddata input file
|
// MaxRestoreSize defines the max size for the loaddata input file
|
||||||
MaxRestoreSize = 10485760 // 10 MB
|
MaxRestoreSize = 10485760 // 10 MB
|
||||||
maxRequestSize = 1048576 // 1MB
|
maxRequestSize = 1048576 // 1MB
|
||||||
maxLoginPostSize = 262144 // 256 KB
|
maxLoginBodySize = 262144 // 256 KB
|
||||||
maxMultipartMem = 8388608 // 8MB
|
maxMultipartMem = 8388608 // 8MB
|
||||||
osWindows = "windows"
|
osWindows = "windows"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
backupsPath string
|
backupsPath string
|
||||||
certMgr *common.CertManager
|
certMgr *common.CertManager
|
||||||
jwtTokensCleanupTicker *time.Ticker
|
jwtTokensCleanupTicker *time.Ticker
|
||||||
jwtTokensCleanupDone chan bool
|
jwtTokensCleanupDone chan bool
|
||||||
invalidatedJWTTokens sync.Map
|
invalidatedJWTTokens sync.Map
|
||||||
csrfTokenAuth *jwtauth.JWTAuth
|
csrfTokenAuth *jwtauth.JWTAuth
|
||||||
webRootPath string
|
webRootPath string
|
||||||
webBasePath string
|
webBasePath string
|
||||||
webBaseAdminPath string
|
webBaseAdminPath string
|
||||||
webBaseClientPath string
|
webBaseClientPath string
|
||||||
webAdminSetupPath string
|
webAdminSetupPath string
|
||||||
webLoginPath string
|
webLoginPath string
|
||||||
webLogoutPath string
|
webLogoutPath string
|
||||||
webUsersPath string
|
webUsersPath string
|
||||||
webUserPath string
|
webUserPath string
|
||||||
webConnectionsPath string
|
webConnectionsPath string
|
||||||
webFoldersPath string
|
webFoldersPath string
|
||||||
webFolderPath string
|
webFolderPath string
|
||||||
webStatusPath string
|
webStatusPath string
|
||||||
webAdminsPath string
|
webAdminsPath string
|
||||||
webAdminPath string
|
webAdminPath string
|
||||||
webMaintenancePath string
|
webMaintenancePath string
|
||||||
webBackupPath string
|
webBackupPath string
|
||||||
webRestorePath string
|
webRestorePath string
|
||||||
webScanVFolderPath string
|
webScanVFolderPath string
|
||||||
webQuotaScanPath string
|
webQuotaScanPath string
|
||||||
webChangeAdminPwdPath string
|
webAdminCredentialsPath string
|
||||||
webTemplateUser string
|
webChangeAdminAPIKeyAccessPath string
|
||||||
webTemplateFolder string
|
webChangeAdminPwdPath string
|
||||||
webDefenderPath string
|
webTemplateUser string
|
||||||
webDefenderHostsPath string
|
webTemplateFolder string
|
||||||
webClientLoginPath string
|
webDefenderPath string
|
||||||
webClientFilesPath string
|
webDefenderHostsPath string
|
||||||
webClientDirsPath string
|
webClientLoginPath string
|
||||||
webClientDownloadZipPath string
|
webClientFilesPath string
|
||||||
webClientCredentialsPath string
|
webClientDirsPath string
|
||||||
webChangeClientPwdPath string
|
webClientDownloadZipPath string
|
||||||
webChangeClientKeysPath string
|
webClientCredentialsPath string
|
||||||
webClientLogoutPath string
|
webChangeClientPwdPath string
|
||||||
webStaticFilesPath string
|
webChangeClientKeysPath string
|
||||||
|
webChangeClientAPIKeyAccessPath string
|
||||||
|
webClientLogoutPath string
|
||||||
|
webStaticFilesPath string
|
||||||
// max upload size for http clients, 1GB by default
|
// max upload size for http clients, 1GB by default
|
||||||
maxUploadFileSize = int64(1048576000)
|
maxUploadFileSize = int64(1048576000)
|
||||||
)
|
)
|
||||||
|
@ -478,6 +485,7 @@ func updateWebClientURLs(baseURL string) {
|
||||||
webClientCredentialsPath = path.Join(baseURL, webClientCredentialsPathDefault)
|
webClientCredentialsPath = path.Join(baseURL, webClientCredentialsPathDefault)
|
||||||
webChangeClientPwdPath = path.Join(baseURL, webChangeClientPwdPathDefault)
|
webChangeClientPwdPath = path.Join(baseURL, webChangeClientPwdPathDefault)
|
||||||
webChangeClientKeysPath = path.Join(baseURL, webChangeClientKeysPathDefault)
|
webChangeClientKeysPath = path.Join(baseURL, webChangeClientKeysPathDefault)
|
||||||
|
webChangeClientAPIKeyAccessPath = path.Join(baseURL, webChangeClientAPIKeyAccessPathDefault)
|
||||||
webClientLogoutPath = path.Join(baseURL, webClientLogoutPathDefault)
|
webClientLogoutPath = path.Join(baseURL, webClientLogoutPathDefault)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -505,6 +513,8 @@ func updateWebAdminURLs(baseURL string) {
|
||||||
webScanVFolderPath = path.Join(baseURL, webScanVFolderPathDefault)
|
webScanVFolderPath = path.Join(baseURL, webScanVFolderPathDefault)
|
||||||
webQuotaScanPath = path.Join(baseURL, webQuotaScanPathDefault)
|
webQuotaScanPath = path.Join(baseURL, webQuotaScanPathDefault)
|
||||||
webChangeAdminPwdPath = path.Join(baseURL, webChangeAdminPwdPathDefault)
|
webChangeAdminPwdPath = path.Join(baseURL, webChangeAdminPwdPathDefault)
|
||||||
|
webAdminCredentialsPath = path.Join(baseURL, webAdminCredentialsPathDefault)
|
||||||
|
webChangeAdminAPIKeyAccessPath = path.Join(baseURL, webChangeAdminAPIKeyAccessPathDefault)
|
||||||
webTemplateUser = path.Join(baseURL, webTemplateUserDefault)
|
webTemplateUser = path.Join(baseURL, webTemplateUserDefault)
|
||||||
webTemplateFolder = path.Join(baseURL, webTemplateFolderDefault)
|
webTemplateFolder = path.Join(baseURL, webTemplateFolderDefault)
|
||||||
webDefenderHostsPath = path.Join(baseURL, webDefenderHostsPathDefault)
|
webDefenderHostsPath = path.Join(baseURL, webDefenderHostsPathDefault)
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -516,7 +516,9 @@ func TestCreateTokenError(t *testing.T) {
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
rr = httptest.NewRecorder()
|
rr = httptest.NewRecorder()
|
||||||
handleWebAdminChangePwdPost(rr, req)
|
handleWebAdminChangePwdPost(rr, req)
|
||||||
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
|
// the claim is invalid so we fail to render the client page since
|
||||||
|
// we have to load the logged admin
|
||||||
|
assert.Equal(t, http.StatusInternalServerError, rr.Code, rr.Body.String())
|
||||||
|
|
||||||
req, _ = http.NewRequest(http.MethodGet, webLoginPath+"?a=a%C3%A2%G3", nil)
|
req, _ = http.NewRequest(http.MethodGet, webLoginPath+"?a=a%C3%A2%G3", nil)
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
@ -541,6 +543,18 @@ func TestCreateTokenError(t *testing.T) {
|
||||||
handleWebClientManageKeysPost(rr, req)
|
handleWebClientManageKeysPost(rr, req)
|
||||||
assert.Equal(t, http.StatusInternalServerError, rr.Code, rr.Body.String())
|
assert.Equal(t, http.StatusInternalServerError, rr.Code, rr.Body.String())
|
||||||
|
|
||||||
|
req, _ = http.NewRequest(http.MethodPost, webChangeClientAPIKeyAccessPath+"?a=a%C3%AO%GA", bytes.NewBuffer([]byte(form.Encode())))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
handleWebClientManageAPIKeyPost(rr, req)
|
||||||
|
assert.Equal(t, http.StatusInternalServerError, rr.Code, rr.Body.String())
|
||||||
|
|
||||||
|
req, _ = http.NewRequest(http.MethodPost, webChangeAdminAPIKeyAccessPath+"?a=a%C3%AO%GB", bytes.NewBuffer([]byte(form.Encode())))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
handleWebAdminManageAPIKeyPost(rr, req)
|
||||||
|
assert.Equal(t, http.StatusInternalServerError, rr.Code, rr.Body.String())
|
||||||
|
|
||||||
username := "webclientuser"
|
username := "webclientuser"
|
||||||
user = dataprovider.User{
|
user = dataprovider.User{
|
||||||
BaseUser: sdk.BaseUser{
|
BaseUser: sdk.BaseUser{
|
||||||
|
@ -552,7 +566,8 @@ func TestCreateTokenError(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
user.Permissions = make(map[string][]string)
|
user.Permissions = make(map[string][]string)
|
||||||
user.Permissions["/"] = []string{"*"}
|
user.Permissions["/"] = []string{dataprovider.PermAny}
|
||||||
|
user.Filters.AllowAPIKeyAuth = true
|
||||||
err = dataprovider.AddUser(&user)
|
err = dataprovider.AddUser(&user)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
@ -567,8 +582,34 @@ func TestCreateTokenError(t *testing.T) {
|
||||||
server.handleWebClientLoginPost(rr, req)
|
server.handleWebClientLoginPost(rr, req)
|
||||||
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
|
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
|
||||||
|
|
||||||
|
err = authenticateUserWithAPIKey(username, "", server.tokenAuth, req)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
err = dataprovider.DeleteUser(username)
|
err = dataprovider.DeleteUser(username)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
admin.Username += "1"
|
||||||
|
admin.Status = 1
|
||||||
|
admin.Filters.AllowAPIKeyAuth = true
|
||||||
|
admin.Permissions = []string{dataprovider.PermAdminAny}
|
||||||
|
err = dataprovider.AddAdmin(&admin)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = authenticateAdminWithAPIKey(admin.Username, "", server.tokenAuth, req)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
err = dataprovider.DeleteAdmin(admin.Username)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIKeyAuthForbidden(t *testing.T) {
|
||||||
|
r := GetHTTPRouter()
|
||||||
|
fn := forbidAPIKeyAuthentication(r)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, versionPath, nil)
|
||||||
|
fn.ServeHTTP(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestJWTTokenValidation(t *testing.T) {
|
func TestJWTTokenValidation(t *testing.T) {
|
||||||
|
@ -1594,7 +1635,7 @@ func TestGetFilesInvalidClaims(t *testing.T) {
|
||||||
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestManageKeysInvalidClaims(t *testing.T) {
|
func TestInvalidClaims(t *testing.T) {
|
||||||
server := httpdServer{}
|
server := httpdServer{}
|
||||||
server.initializeRouter()
|
server.initializeRouter()
|
||||||
|
|
||||||
|
@ -1620,7 +1661,35 @@ func TestManageKeysInvalidClaims(t *testing.T) {
|
||||||
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
|
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
|
||||||
handleWebClientManageKeysPost(rr, req)
|
handleWebClientManageKeysPost(rr, req)
|
||||||
assert.Equal(t, http.StatusInternalServerError, rr.Code)
|
assert.Equal(t, http.StatusInternalServerError, rr.Code)
|
||||||
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
|
||||||
|
form = make(url.Values)
|
||||||
|
form.Set(csrfFormToken, createCSRFToken())
|
||||||
|
form.Set("allow_api_key_auth", "")
|
||||||
|
req, _ = http.NewRequest(http.MethodPost, webChangeClientKeysPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
|
||||||
|
handleWebClientManageAPIKeyPost(rr, req)
|
||||||
|
assert.Equal(t, http.StatusInternalServerError, rr.Code)
|
||||||
|
|
||||||
|
admin := dataprovider.Admin{
|
||||||
|
Username: "",
|
||||||
|
Password: user.Password,
|
||||||
|
}
|
||||||
|
c = jwtTokenClaims{
|
||||||
|
Username: admin.Username,
|
||||||
|
Permissions: nil,
|
||||||
|
Signature: admin.GetSignature(),
|
||||||
|
}
|
||||||
|
token, err = c.createTokenResponse(server.tokenAuth, tokenAudienceWebAdmin)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
form = make(url.Values)
|
||||||
|
form.Set(csrfFormToken, createCSRFToken())
|
||||||
|
form.Set("allow_api_key_auth", "")
|
||||||
|
req, _ = http.NewRequest(http.MethodPost, webChangeClientKeysPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
|
||||||
|
handleWebAdminManageAPIKeyPost(rr, req)
|
||||||
|
assert.Equal(t, http.StatusInternalServerError, rr.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTLSReq(t *testing.T) {
|
func TestTLSReq(t *testing.T) {
|
||||||
|
|
|
@ -2,14 +2,21 @@ package httpd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
"github.com/go-chi/jwtauth/v5"
|
"github.com/go-chi/jwtauth/v5"
|
||||||
"github.com/lestrrat-go/jwx/jwt"
|
"github.com/lestrrat-go/jwx/jwt"
|
||||||
|
"github.com/rs/xid"
|
||||||
|
|
||||||
|
"github.com/drakkan/sftpgo/v2/common"
|
||||||
|
"github.com/drakkan/sftpgo/v2/dataprovider"
|
||||||
"github.com/drakkan/sftpgo/v2/logger"
|
"github.com/drakkan/sftpgo/v2/logger"
|
||||||
|
"github.com/drakkan/sftpgo/v2/sdk"
|
||||||
"github.com/drakkan/sftpgo/v2/util"
|
"github.com/drakkan/sftpgo/v2/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -201,6 +208,88 @@ func verifyCSRFHeader(next http.Handler) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkAPIKeyAuth(tokenAuth *jwtauth.JWTAuth, scope dataprovider.APIKeyScope) func(next http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
apiKey := r.Header.Get("X-SFTPGO-API-KEY")
|
||||||
|
if apiKey == "" {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keyParams := strings.SplitN(apiKey, ".", 3)
|
||||||
|
if len(keyParams) < 2 {
|
||||||
|
logger.Debug(logSender, "", "invalid api key %#v", apiKey)
|
||||||
|
sendAPIResponse(w, r, errors.New("the provided api key is not valid"), "", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keyID := keyParams[0]
|
||||||
|
key := keyParams[1]
|
||||||
|
apiUser := ""
|
||||||
|
if len(keyParams) > 2 {
|
||||||
|
apiUser = keyParams[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
k, err := dataprovider.APIKeyExists(keyID)
|
||||||
|
if err != nil {
|
||||||
|
logger.Debug(logSender, "invalid api key %#v: %v", apiKey, err)
|
||||||
|
sendAPIResponse(w, r, errors.New("the provided api key is not valid"), "", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := k.Authenticate(key); err != nil {
|
||||||
|
logger.Debug(logSender, "unable to authenticate api key %#v: %v", apiKey, err)
|
||||||
|
sendAPIResponse(w, r, fmt.Errorf("the provided api key cannot be authenticated"), "", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if scope == dataprovider.APIKeyScopeAdmin {
|
||||||
|
if k.Admin != "" {
|
||||||
|
apiUser = k.Admin
|
||||||
|
}
|
||||||
|
if err := authenticateAdminWithAPIKey(apiUser, keyID, tokenAuth, r); err != nil {
|
||||||
|
logger.Debug(logSender, "", "unable to authenticate admin %#v associated with api key %#v: %v",
|
||||||
|
apiUser, apiKey, err)
|
||||||
|
sendAPIResponse(w, r, fmt.Errorf("the admin associated with the provided api key cannot be authenticated"),
|
||||||
|
"", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if k.User != "" {
|
||||||
|
apiUser = k.User
|
||||||
|
}
|
||||||
|
if err := authenticateUserWithAPIKey(apiUser, keyID, tokenAuth, r); err != nil {
|
||||||
|
logger.Debug(logSender, "", "unable to authenticate user %#v associated with api key %#v: %v",
|
||||||
|
apiUser, apiKey, err)
|
||||||
|
code := http.StatusUnauthorized
|
||||||
|
if errors.Is(err, common.ErrInternalFailure) {
|
||||||
|
code = http.StatusInternalServerError
|
||||||
|
}
|
||||||
|
sendAPIResponse(w, r, errors.New("the user associated with the provided api key cannot be authenticated"),
|
||||||
|
"", code)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataprovider.UpdateAPIKeyLastUse(&k) //nolint:errcheck
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func forbidAPIKeyAuthentication(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if claims.APIKeyID != "" {
|
||||||
|
sendAPIResponse(w, r, nil, "API key authentication is not allowed", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func recoverer(next http.Handler) http.Handler {
|
func recoverer(next http.Handler) http.Handler {
|
||||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -225,3 +314,91 @@ func recoverer(next http.Handler) http.Handler {
|
||||||
|
|
||||||
return http.HandlerFunc(fn)
|
return http.HandlerFunc(fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func authenticateAdminWithAPIKey(username, keyID string, tokenAuth *jwtauth.JWTAuth, r *http.Request) error {
|
||||||
|
if username == "" {
|
||||||
|
return errors.New("the provided key is not associated with any admin and no username was provided")
|
||||||
|
}
|
||||||
|
admin, err := dataprovider.AdminExists(username)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !admin.Filters.AllowAPIKeyAuth {
|
||||||
|
return fmt.Errorf("API key authentication disabled for admin %#v", admin.Username)
|
||||||
|
}
|
||||||
|
if err := admin.CanLogin(util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c := jwtTokenClaims{
|
||||||
|
Username: admin.Username,
|
||||||
|
Permissions: admin.Permissions,
|
||||||
|
Signature: admin.GetSignature(),
|
||||||
|
APIKeyID: keyID,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.createTokenResponse(tokenAuth, tokenAudienceAPI)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.Header.Set("Authorization", fmt.Sprintf("Bearer %v", resp["access_token"]))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func authenticateUserWithAPIKey(username, keyID string, tokenAuth *jwtauth.JWTAuth, r *http.Request) error {
|
||||||
|
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||||
|
if username == "" {
|
||||||
|
err := errors.New("the provided key is not associated with any user and no username was provided")
|
||||||
|
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}}, ipAddr, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := common.Config.ExecutePostConnectHook(ipAddr, common.ProtocolHTTP); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
user, err := dataprovider.UserExists(username)
|
||||||
|
if err != nil {
|
||||||
|
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}}, ipAddr, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !user.Filters.AllowAPIKeyAuth {
|
||||||
|
err := fmt.Errorf("API key authentication disabled for user %#v", user.Username)
|
||||||
|
updateLoginMetrics(&user, ipAddr, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := user.CheckLoginConditions(); err != nil {
|
||||||
|
updateLoginMetrics(&user, ipAddr, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
connectionID := fmt.Sprintf("%v_%v", common.ProtocolHTTP, xid.New().String())
|
||||||
|
if err := checkHTTPClientUser(&user, r, connectionID); err != nil {
|
||||||
|
updateLoginMetrics(&user, ipAddr, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
lastLogin := util.GetTimeFromMsecSinceEpoch(user.LastLogin)
|
||||||
|
diff := -time.Until(lastLogin)
|
||||||
|
if diff < 0 || diff > 10*time.Minute {
|
||||||
|
defer user.CloseFs() //nolint:errcheck
|
||||||
|
err = user.CheckFsRoot(connectionID)
|
||||||
|
if err != nil {
|
||||||
|
updateLoginMetrics(&user, ipAddr, common.ErrInternalFailure)
|
||||||
|
return common.ErrInternalFailure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c := jwtTokenClaims{
|
||||||
|
Username: user.Username,
|
||||||
|
Permissions: user.Filters.WebClient,
|
||||||
|
Signature: user.GetSignature(),
|
||||||
|
APIKeyID: keyID,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.createTokenResponse(tokenAuth, tokenAudienceAPIUser)
|
||||||
|
if err != nil {
|
||||||
|
updateLoginMetrics(&user, ipAddr, common.ErrInternalFailure)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.Header.Set("Authorization", fmt.Sprintf("Bearer %v", resp["access_token"]))
|
||||||
|
dataprovider.UpdateLastLogin(&user) //nolint:errcheck
|
||||||
|
updateLoginMetrics(&user, ipAddr, nil)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ tags:
|
||||||
- name: token
|
- name: token
|
||||||
- name: maintenance
|
- name: maintenance
|
||||||
- name: admins
|
- name: admins
|
||||||
|
- name: API keys
|
||||||
- name: connections
|
- name: connections
|
||||||
- name: defender
|
- name: defender
|
||||||
- name: quota
|
- name: quota
|
||||||
|
@ -28,6 +29,7 @@ servers:
|
||||||
- url: /api/v2
|
- url: /api/v2
|
||||||
security:
|
security:
|
||||||
- BearerAuth: []
|
- BearerAuth: []
|
||||||
|
- APIKeyAuth: []
|
||||||
paths:
|
paths:
|
||||||
/healthz:
|
/healthz:
|
||||||
get:
|
get:
|
||||||
|
@ -73,6 +75,8 @@ paths:
|
||||||
$ref: '#/components/responses/DefaultResponse'
|
$ref: '#/components/responses/DefaultResponse'
|
||||||
/logout:
|
/logout:
|
||||||
get:
|
get:
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
tags:
|
tags:
|
||||||
- token
|
- token
|
||||||
summary: Invalidate an admin access token
|
summary: Invalidate an admin access token
|
||||||
|
@ -119,6 +123,8 @@ paths:
|
||||||
$ref: '#/components/responses/DefaultResponse'
|
$ref: '#/components/responses/DefaultResponse'
|
||||||
/user/logout:
|
/user/logout:
|
||||||
get:
|
get:
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
tags:
|
tags:
|
||||||
- token
|
- token
|
||||||
summary: Invalidate a user access token
|
summary: Invalidate a user access token
|
||||||
|
@ -163,6 +169,8 @@ paths:
|
||||||
$ref: '#/components/responses/DefaultResponse'
|
$ref: '#/components/responses/DefaultResponse'
|
||||||
/changepwd/admin:
|
/changepwd/admin:
|
||||||
put:
|
put:
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
tags:
|
tags:
|
||||||
- admins
|
- admins
|
||||||
summary: Change admin password
|
summary: Change admin password
|
||||||
|
@ -192,6 +200,8 @@ paths:
|
||||||
$ref: '#/components/responses/DefaultResponse'
|
$ref: '#/components/responses/DefaultResponse'
|
||||||
/admin/changepwd:
|
/admin/changepwd:
|
||||||
put:
|
put:
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
tags:
|
tags:
|
||||||
- admins
|
- admins
|
||||||
summary: Change admin password
|
summary: Change admin password
|
||||||
|
@ -1123,6 +1133,207 @@ paths:
|
||||||
$ref: '#/components/responses/InternalServerError'
|
$ref: '#/components/responses/InternalServerError'
|
||||||
default:
|
default:
|
||||||
$ref: '#/components/responses/DefaultResponse'
|
$ref: '#/components/responses/DefaultResponse'
|
||||||
|
/apikeys:
|
||||||
|
get:
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
tags:
|
||||||
|
- API keys
|
||||||
|
summary: Get API keys
|
||||||
|
description: Returns an array with one or more API keys. For security reasons hashed keys are omitted in the response
|
||||||
|
operationId: get_api_keys
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: offset
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
minimum: 0
|
||||||
|
default: 0
|
||||||
|
required: false
|
||||||
|
- in: query
|
||||||
|
name: limit
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
minimum: 1
|
||||||
|
maximum: 500
|
||||||
|
default: 100
|
||||||
|
required: false
|
||||||
|
description: 'The maximum number of items to return. Max value is 500, default is 100'
|
||||||
|
- in: query
|
||||||
|
name: order
|
||||||
|
required: false
|
||||||
|
description: Ordering API keys by id. Default ASC
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- ASC
|
||||||
|
- DESC
|
||||||
|
example: ASC
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/APIKey'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/Unauthorized'
|
||||||
|
'403':
|
||||||
|
$ref: '#/components/responses/Forbidden'
|
||||||
|
'500':
|
||||||
|
$ref: '#/components/responses/InternalServerError'
|
||||||
|
default:
|
||||||
|
$ref: '#/components/responses/DefaultResponse'
|
||||||
|
post:
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
tags:
|
||||||
|
- API keys
|
||||||
|
summary: Add API key
|
||||||
|
description: Adds a new API key
|
||||||
|
operationId: add_api_key
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/APIKey'
|
||||||
|
responses:
|
||||||
|
'201':
|
||||||
|
description: successful operation
|
||||||
|
headers:
|
||||||
|
X-Object-ID:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: ID for the new created API key
|
||||||
|
Location:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: URL to retrieve the details for the new created API key
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
mesage:
|
||||||
|
type: string
|
||||||
|
example: 'API key created. This is the only time the API key is visible, please save it.'
|
||||||
|
key:
|
||||||
|
type: string
|
||||||
|
description: 'generated API key'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/Unauthorized'
|
||||||
|
'403':
|
||||||
|
$ref: '#/components/responses/Forbidden'
|
||||||
|
'500':
|
||||||
|
$ref: '#/components/responses/InternalServerError'
|
||||||
|
default:
|
||||||
|
$ref: '#/components/responses/DefaultResponse'
|
||||||
|
'/apikeys/{id}':
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
description: the key id
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
get:
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
tags:
|
||||||
|
- API keys
|
||||||
|
summary: Find API key by id
|
||||||
|
description: Returns the API key with the given id, if it exists. For security reasons the hashed key is omitted in the response
|
||||||
|
operationId: get_api_key_by_id
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/APIKey'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/Unauthorized'
|
||||||
|
'403':
|
||||||
|
$ref: '#/components/responses/Forbidden'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
'500':
|
||||||
|
$ref: '#/components/responses/InternalServerError'
|
||||||
|
default:
|
||||||
|
$ref: '#/components/responses/DefaultResponse'
|
||||||
|
put:
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
tags:
|
||||||
|
- API keys
|
||||||
|
summary: Update API key
|
||||||
|
description: Updates an existing API key. You cannot update the key itself, the creation date and the last use
|
||||||
|
operationId: update_api_key
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/APIKey'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ApiResponse'
|
||||||
|
example:
|
||||||
|
message: API key updated
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/Unauthorized'
|
||||||
|
'403':
|
||||||
|
$ref: '#/components/responses/Forbidden'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
'500':
|
||||||
|
$ref: '#/components/responses/InternalServerError'
|
||||||
|
default:
|
||||||
|
$ref: '#/components/responses/DefaultResponse'
|
||||||
|
delete:
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
tags:
|
||||||
|
- API keys
|
||||||
|
summary: Delete API key
|
||||||
|
description: Deletes an existing API key
|
||||||
|
operationId: delete_api_key
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ApiResponse'
|
||||||
|
example:
|
||||||
|
message: Admin deleted
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/Unauthorized'
|
||||||
|
'403':
|
||||||
|
$ref: '#/components/responses/Forbidden'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
'500':
|
||||||
|
$ref: '#/components/responses/InternalServerError'
|
||||||
|
default:
|
||||||
|
$ref: '#/components/responses/DefaultResponse'
|
||||||
/admins:
|
/admins:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
@ -1258,7 +1469,7 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- admins
|
- admins
|
||||||
summary: Update admin
|
summary: Update admin
|
||||||
description: Updates an existing admin
|
description: Updates an existing admin. You are not allowed to update the admin impersonated using an API key
|
||||||
operationId: update_admin
|
operationId: update_admin
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
|
@ -1605,9 +1816,9 @@ paths:
|
||||||
- 2
|
- 2
|
||||||
description: |
|
description: |
|
||||||
Mode:
|
Mode:
|
||||||
* `0` New users/admins are added, existing users/admins are updated. This is the default
|
* `0` New users/admins/API keys are added, existing ones are updated. This is the default
|
||||||
* `1` New users/admins are added, existing users/admins are not modified
|
* `1` New users/admins/API keys are added, existing ones are not modified
|
||||||
* `2` New users are added, existing users are updated and, if connected, they are disconnected and so forced to use the new configuration
|
* `2` New users/admins/API keys are added, existing ones are updated and connected users are disconnected and so forced to use the new configuration
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- maintenance
|
- maintenance
|
||||||
|
@ -1673,6 +1884,8 @@ paths:
|
||||||
$ref: '#/components/responses/DefaultResponse'
|
$ref: '#/components/responses/DefaultResponse'
|
||||||
/user/changepwd:
|
/user/changepwd:
|
||||||
put:
|
put:
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
tags:
|
tags:
|
||||||
- users API
|
- users API
|
||||||
summary: Change user password
|
summary: Change user password
|
||||||
|
@ -1701,6 +1914,8 @@ paths:
|
||||||
$ref: '#/components/responses/DefaultResponse'
|
$ref: '#/components/responses/DefaultResponse'
|
||||||
/user/publickeys:
|
/user/publickeys:
|
||||||
get:
|
get:
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
tags:
|
tags:
|
||||||
- users API
|
- users API
|
||||||
summary: Get the user's public keys
|
summary: Get the user's public keys
|
||||||
|
@ -1726,6 +1941,8 @@ paths:
|
||||||
default:
|
default:
|
||||||
$ref: '#/components/responses/DefaultResponse'
|
$ref: '#/components/responses/DefaultResponse'
|
||||||
put:
|
put:
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
tags:
|
tags:
|
||||||
- users API
|
- users API
|
||||||
summary: Set the user's public keys
|
summary: Set the user's public keys
|
||||||
|
@ -2248,6 +2465,7 @@ components:
|
||||||
- close_conns
|
- close_conns
|
||||||
- view_status
|
- view_status
|
||||||
- manage_admins
|
- manage_admins
|
||||||
|
- manage_apikeys
|
||||||
- quota_scans
|
- quota_scans
|
||||||
- manage_system
|
- manage_system
|
||||||
- manage_defender
|
- manage_defender
|
||||||
|
@ -2263,6 +2481,7 @@ components:
|
||||||
* `close_conns` - close active connections is allowed
|
* `close_conns` - close active connections is allowed
|
||||||
* `view_status` - view the server status is allowed
|
* `view_status` - view the server status is allowed
|
||||||
* `manage_admins` - manage other admins is allowed
|
* `manage_admins` - manage other admins is allowed
|
||||||
|
* `manage_apikeys` - manage API keys is allowed
|
||||||
* `manage_defender` - remove ip from the dynamic blocklist is allowed
|
* `manage_defender` - remove ip from the dynamic blocklist is allowed
|
||||||
* `view_defender` - list the dynamic blocklist is allowed
|
* `view_defender` - list the dynamic blocklist is allowed
|
||||||
LoginMethods:
|
LoginMethods:
|
||||||
|
@ -2306,6 +2525,15 @@ components:
|
||||||
Options:
|
Options:
|
||||||
* `publickey-change-disabled` - changing SSH public keys is not allowed
|
* `publickey-change-disabled` - changing SSH public keys is not allowed
|
||||||
* `write-disabled` - upload, rename, delete are not allowed even if the user has permissions for these actions
|
* `write-disabled` - upload, rename, delete are not allowed even if the user has permissions for these actions
|
||||||
|
APIKeyScope:
|
||||||
|
type: integer
|
||||||
|
enum:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
description: |
|
||||||
|
Options:
|
||||||
|
* `1` - admin scope. The API key will be used to impersonate an SFTPGo admin
|
||||||
|
* `2` - user scope. The API key will be used to impersonate an SFTPGo user
|
||||||
PatternsFilter:
|
PatternsFilter:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -2397,6 +2625,9 @@ components:
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/WebClientOptions'
|
$ref: '#/components/schemas/WebClientOptions'
|
||||||
description: WebClient/user REST API related configuration options
|
description: WebClient/user REST API related configuration options
|
||||||
|
allow_api_key_auth:
|
||||||
|
type: boolean
|
||||||
|
description: 'API key authentication allows to impersonate this user with an API key'
|
||||||
description: Additional user options
|
description: Additional user options
|
||||||
Secret:
|
Secret:
|
||||||
type: object
|
type: object
|
||||||
|
@ -2760,6 +2991,9 @@ components:
|
||||||
example:
|
example:
|
||||||
- 192.0.2.0/24
|
- 192.0.2.0/24
|
||||||
- '2001:db8::/32'
|
- '2001:db8::/32'
|
||||||
|
allow_api_key_auth:
|
||||||
|
type: boolean
|
||||||
|
description: 'API key auth allows to impersonate this administrator with an API key'
|
||||||
Admin:
|
Admin:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -2798,6 +3032,46 @@ components:
|
||||||
additional_info:
|
additional_info:
|
||||||
type: string
|
type: string
|
||||||
description: Free form text field
|
description: Free form text field
|
||||||
|
APIKey:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
description: unique key identifier
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: User friendly key name
|
||||||
|
key:
|
||||||
|
type: string
|
||||||
|
format: password
|
||||||
|
description: We store the hash of the key. This is just like a password. For security reasons this field is omitted when you search/get API keys
|
||||||
|
scope:
|
||||||
|
$ref: '#/components/schemas/APIKeyScope'
|
||||||
|
created_at:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
description: creation time as unix timestamp in milliseconds
|
||||||
|
updated_at:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
description: last update time as unix timestamp in milliseconds
|
||||||
|
last_use_at:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
description: last use time as unix timestamp in milliseconds. It is saved at most once every 10 minutes
|
||||||
|
expires_at:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
description: expiration time as unix timestamp in milliseconds
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
description: optional description
|
||||||
|
user:
|
||||||
|
type: string
|
||||||
|
description: username associated with this API key. If empty and the scope is "user scope" the key can impersonate any user
|
||||||
|
admin:
|
||||||
|
type: string
|
||||||
|
description: admin associated with this API key. If empty and the scope is "admin scope" the key can impersonate any admin
|
||||||
QuotaUsage:
|
QuotaUsage:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -3063,6 +3337,8 @@ components:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/Admin'
|
$ref: '#/components/schemas/Admin'
|
||||||
|
api_keys:
|
||||||
|
$ref: '#/components/schemas/APIKey'
|
||||||
version:
|
version:
|
||||||
type: integer
|
type: integer
|
||||||
PwdChange:
|
PwdChange:
|
||||||
|
@ -3132,3 +3408,8 @@ components:
|
||||||
type: http
|
type: http
|
||||||
scheme: bearer
|
scheme: bearer
|
||||||
bearerFormat: JWT
|
bearerFormat: JWT
|
||||||
|
APIKeyAuth:
|
||||||
|
type: apiKey
|
||||||
|
in: header
|
||||||
|
name: X-SFTPGO-API-KEY
|
||||||
|
description: 'API key to use for authentication. API key authentication is intrinsically less secure than using a short lived JWT token. You should prefer API key authentication only for machine-to-machine communications in trusted environments. If no admin/user is associated to the provided key you need to add ".username" at the end of the key. For example if your API key is "6ajKLwswLccVBGpZGv596G.ySAXc8vtp9hMiwAuaLtzof" and you want to impersonate the admin with username "myadmin" you have to use "6ajKLwswLccVBGpZGv596G.ySAXc8vtp9hMiwAuaLtzof.myadmin" as API key. When using API key authentication you cannot manage API keys, update the impersonated admin, change password or public keys for the impersonated user.'
|
||||||
|
|
|
@ -131,11 +131,12 @@ func (s *httpdServer) renderClientLoginPage(w http.ResponseWriter, error string)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *httpdServer) handleClientWebLogin(w http.ResponseWriter, r *http.Request) {
|
func (s *httpdServer) handleClientWebLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
|
||||||
s.renderClientLoginPage(w, "")
|
s.renderClientLoginPage(w, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *httpdServer) handleWebClientLoginPost(w http.ResponseWriter, r *http.Request) {
|
func (s *httpdServer) handleWebClientLoginPost(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxLoginPostSize)
|
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
|
||||||
|
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
s.renderClientLoginPage(w, err.Error())
|
s.renderClientLoginPage(w, err.Error())
|
||||||
|
@ -201,7 +202,7 @@ func (s *httpdServer) handleWebClientLoginPost(w http.ResponseWriter, r *http.Re
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *httpdServer) handleWebAdminLoginPost(w http.ResponseWriter, r *http.Request) {
|
func (s *httpdServer) handleWebAdminLoginPost(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxLoginPostSize)
|
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
s.renderAdminLoginPage(w, err.Error())
|
s.renderAdminLoginPage(w, err.Error())
|
||||||
return
|
return
|
||||||
|
@ -239,6 +240,7 @@ func (s *httpdServer) renderAdminLoginPage(w http.ResponseWriter, error string)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *httpdServer) handleWebAdminLogin(w http.ResponseWriter, r *http.Request) {
|
func (s *httpdServer) handleWebAdminLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
|
||||||
if !dataprovider.HasAdmin() {
|
if !dataprovider.HasAdmin() {
|
||||||
http.Redirect(w, r, webAdminSetupPath, http.StatusFound)
|
http.Redirect(w, r, webAdminSetupPath, http.StatusFound)
|
||||||
return
|
return
|
||||||
|
@ -247,7 +249,7 @@ func (s *httpdServer) handleWebAdminLogin(w http.ResponseWriter, r *http.Request
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *httpdServer) handleWebAdminSetupPost(w http.ResponseWriter, r *http.Request) {
|
func (s *httpdServer) handleWebAdminSetupPost(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxLoginPostSize)
|
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
|
||||||
if dataprovider.HasAdmin() {
|
if dataprovider.HasAdmin() {
|
||||||
renderBadRequestPage(w, r, errors.New("an admin user already exists"))
|
renderBadRequestPage(w, r, errors.New("an admin user already exists"))
|
||||||
return
|
return
|
||||||
|
@ -308,11 +310,13 @@ func (s *httpdServer) loginAdmin(w http.ResponseWriter, r *http.Request, admin *
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *httpdServer) logout(w http.ResponseWriter, r *http.Request) {
|
func (s *httpdServer) logout(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
|
||||||
invalidateToken(r)
|
invalidateToken(r)
|
||||||
sendAPIResponse(w, r, nil, "Your token has been invalidated", http.StatusOK)
|
sendAPIResponse(w, r, nil, "Your token has been invalidated", http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *httpdServer) getUserToken(w http.ResponseWriter, r *http.Request) {
|
func (s *httpdServer) getUserToken(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
|
||||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||||
username, password, ok := r.BasicAuth()
|
username, password, ok := r.BasicAuth()
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -580,6 +584,7 @@ func (s *httpdServer) initializeRouter() {
|
||||||
s.router.Use(middleware.StripSlashes)
|
s.router.Use(middleware.StripSlashes)
|
||||||
|
|
||||||
s.router.NotFound(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
s.router.NotFound(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
if (s.enableWebAdmin || s.enableWebClient) && isWebRequest(r) {
|
if (s.enableWebAdmin || s.enableWebClient) && isWebRequest(r) {
|
||||||
r = s.updateContextFromCookie(r)
|
r = s.updateContextFromCookie(r)
|
||||||
if s.enableWebClient && (isWebClientRequest(r) || !s.enableWebAdmin) {
|
if s.enableWebClient && (isWebClientRequest(r) || !s.enableWebAdmin) {
|
||||||
|
@ -599,25 +604,29 @@ func (s *httpdServer) initializeRouter() {
|
||||||
s.router.Get(tokenPath, s.getToken)
|
s.router.Get(tokenPath, s.getToken)
|
||||||
|
|
||||||
s.router.Group(func(router chi.Router) {
|
s.router.Group(func(router chi.Router) {
|
||||||
|
router.Use(checkAPIKeyAuth(s.tokenAuth, dataprovider.APIKeyScopeAdmin))
|
||||||
router.Use(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromHeader))
|
router.Use(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromHeader))
|
||||||
router.Use(jwtAuthenticatorAPI)
|
router.Use(jwtAuthenticatorAPI)
|
||||||
|
|
||||||
router.Get(versionPath, func(w http.ResponseWriter, r *http.Request) {
|
router.Get(versionPath, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
render.JSON(w, r, version.Get())
|
render.JSON(w, r, version.Get())
|
||||||
})
|
})
|
||||||
|
|
||||||
router.Get(logoutPath, s.logout)
|
router.With(forbidAPIKeyAuthentication).Get(logoutPath, s.logout)
|
||||||
router.Put(adminPwdPath, changeAdminPassword)
|
router.With(forbidAPIKeyAuthentication).Put(adminPwdPath, changeAdminPassword)
|
||||||
// compatibility layer to remove in v2.2
|
// compatibility layer to remove in v2.2
|
||||||
router.Put(adminPwdCompatPath, changeAdminPassword)
|
router.With(forbidAPIKeyAuthentication).Put(adminPwdCompatPath, changeAdminPassword)
|
||||||
|
|
||||||
router.With(checkPerm(dataprovider.PermAdminViewServerStatus)).
|
router.With(checkPerm(dataprovider.PermAdminViewServerStatus)).
|
||||||
Get(serverStatusPath, func(w http.ResponseWriter, r *http.Request) {
|
Get(serverStatusPath, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
render.JSON(w, r, getServicesStatus())
|
render.JSON(w, r, getServicesStatus())
|
||||||
})
|
})
|
||||||
|
|
||||||
router.With(checkPerm(dataprovider.PermAdminViewConnections)).
|
router.With(checkPerm(dataprovider.PermAdminViewConnections)).
|
||||||
Get(activeConnectionsPath, func(w http.ResponseWriter, r *http.Request) {
|
Get(activeConnectionsPath, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
render.JSON(w, r, common.Connections.GetStats())
|
render.JSON(w, r, common.Connections.GetStats())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -659,18 +668,31 @@ func (s *httpdServer) initializeRouter() {
|
||||||
router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Get(adminPath+"/{username}", getAdminByUsername)
|
router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Get(adminPath+"/{username}", getAdminByUsername)
|
||||||
router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Put(adminPath+"/{username}", updateAdmin)
|
router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Put(adminPath+"/{username}", updateAdmin)
|
||||||
router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Delete(adminPath+"/{username}", deleteAdmin)
|
router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Delete(adminPath+"/{username}", deleteAdmin)
|
||||||
|
router.With(forbidAPIKeyAuthentication, checkPerm(dataprovider.PermAdminManageAPIKeys)).
|
||||||
|
Get(apiKeysPath, getAPIKeys)
|
||||||
|
router.With(forbidAPIKeyAuthentication, checkPerm(dataprovider.PermAdminManageAPIKeys)).
|
||||||
|
Post(apiKeysPath, addAPIKey)
|
||||||
|
router.With(forbidAPIKeyAuthentication, checkPerm(dataprovider.PermAdminManageAPIKeys)).
|
||||||
|
Get(apiKeysPath+"/{id}", getAPIKeyByID)
|
||||||
|
router.With(forbidAPIKeyAuthentication, checkPerm(dataprovider.PermAdminManageAPIKeys)).
|
||||||
|
Put(apiKeysPath+"/{id}", updateAPIKey)
|
||||||
|
router.With(forbidAPIKeyAuthentication, checkPerm(dataprovider.PermAdminManageAPIKeys)).
|
||||||
|
Delete(apiKeysPath+"/{id}", deleteAPIKey)
|
||||||
})
|
})
|
||||||
|
|
||||||
s.router.Get(userTokenPath, s.getUserToken)
|
s.router.Get(userTokenPath, s.getUserToken)
|
||||||
|
|
||||||
s.router.Group(func(router chi.Router) {
|
s.router.Group(func(router chi.Router) {
|
||||||
|
router.Use(checkAPIKeyAuth(s.tokenAuth, dataprovider.APIKeyScopeUser))
|
||||||
router.Use(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromHeader))
|
router.Use(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromHeader))
|
||||||
router.Use(jwtAuthenticatorAPIUser)
|
router.Use(jwtAuthenticatorAPIUser)
|
||||||
|
|
||||||
router.Get(userLogoutPath, s.logout)
|
router.With(forbidAPIKeyAuthentication).Get(userLogoutPath, s.logout)
|
||||||
router.Put(userPwdPath, changeUserPassword)
|
router.With(forbidAPIKeyAuthentication).Put(userPwdPath, changeUserPassword)
|
||||||
router.With(checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).Get(userPublicKeysPath, getUserPublicKeys)
|
router.With(forbidAPIKeyAuthentication, checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).
|
||||||
router.With(checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).Put(userPublicKeysPath, setUserPublicKeys)
|
Get(userPublicKeysPath, getUserPublicKeys)
|
||||||
|
router.With(forbidAPIKeyAuthentication, checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).
|
||||||
|
Put(userPublicKeysPath, setUserPublicKeys)
|
||||||
// compatibility layer to remove in v2.3
|
// compatibility layer to remove in v2.3
|
||||||
router.With(compressor.Handler).Get(userFolderPath, readUserFolder)
|
router.With(compressor.Handler).Get(userFolderPath, readUserFolder)
|
||||||
router.Get(userFilePath, getUserFile)
|
router.Get(userFilePath, getUserFile)
|
||||||
|
@ -693,16 +715,20 @@ func (s *httpdServer) initializeRouter() {
|
||||||
})
|
})
|
||||||
if s.enableWebClient {
|
if s.enableWebClient {
|
||||||
s.router.Get(webRootPath, func(w http.ResponseWriter, r *http.Request) {
|
s.router.Get(webRootPath, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
s.redirectToWebPath(w, r, webClientLoginPath)
|
s.redirectToWebPath(w, r, webClientLoginPath)
|
||||||
})
|
})
|
||||||
s.router.Get(webBasePath, func(w http.ResponseWriter, r *http.Request) {
|
s.router.Get(webBasePath, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
s.redirectToWebPath(w, r, webClientLoginPath)
|
s.redirectToWebPath(w, r, webClientLoginPath)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
s.router.Get(webRootPath, func(w http.ResponseWriter, r *http.Request) {
|
s.router.Get(webRootPath, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
s.redirectToWebPath(w, r, webLoginPath)
|
s.redirectToWebPath(w, r, webLoginPath)
|
||||||
})
|
})
|
||||||
s.router.Get(webBasePath, func(w http.ResponseWriter, r *http.Request) {
|
s.router.Get(webBasePath, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
s.redirectToWebPath(w, r, webLoginPath)
|
s.redirectToWebPath(w, r, webLoginPath)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -710,6 +736,7 @@ func (s *httpdServer) initializeRouter() {
|
||||||
|
|
||||||
if s.enableWebClient {
|
if s.enableWebClient {
|
||||||
s.router.Get(webBaseClientPath, func(w http.ResponseWriter, r *http.Request) {
|
s.router.Get(webBaseClientPath, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
http.Redirect(w, r, webClientLoginPath, http.StatusMovedPermanently)
|
http.Redirect(w, r, webClientLoginPath, http.StatusMovedPermanently)
|
||||||
})
|
})
|
||||||
s.router.Get(webClientLoginPath, s.handleClientWebLogin)
|
s.router.Get(webClientLoginPath, s.handleClientWebLogin)
|
||||||
|
@ -737,6 +764,7 @@ func (s *httpdServer) initializeRouter() {
|
||||||
router.With(s.refreshCookie).Get(webClientDownloadZipPath, handleWebClientDownloadZip)
|
router.With(s.refreshCookie).Get(webClientDownloadZipPath, handleWebClientDownloadZip)
|
||||||
router.With(s.refreshCookie).Get(webClientCredentialsPath, handleClientGetCredentials)
|
router.With(s.refreshCookie).Get(webClientCredentialsPath, handleClientGetCredentials)
|
||||||
router.Post(webChangeClientPwdPath, handleWebClientChangePwdPost)
|
router.Post(webChangeClientPwdPath, handleWebClientChangePwdPost)
|
||||||
|
router.Post(webChangeClientAPIKeyAccessPath, handleWebClientManageAPIKeyPost)
|
||||||
router.With(checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).
|
router.With(checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).
|
||||||
Post(webChangeClientKeysPath, handleWebClientManageKeysPost)
|
Post(webChangeClientKeysPath, handleWebClientManageKeysPost)
|
||||||
})
|
})
|
||||||
|
@ -744,6 +772,7 @@ func (s *httpdServer) initializeRouter() {
|
||||||
|
|
||||||
if s.enableWebAdmin {
|
if s.enableWebAdmin {
|
||||||
s.router.Get(webBaseAdminPath, func(w http.ResponseWriter, r *http.Request) {
|
s.router.Get(webBaseAdminPath, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
|
||||||
s.redirectToWebPath(w, r, webLoginPath)
|
s.redirectToWebPath(w, r, webLoginPath)
|
||||||
})
|
})
|
||||||
s.router.Get(webLoginPath, s.handleWebAdminLogin)
|
s.router.Get(webLoginPath, s.handleWebAdminLogin)
|
||||||
|
@ -756,8 +785,9 @@ func (s *httpdServer) initializeRouter() {
|
||||||
router.Use(jwtAuthenticatorWebAdmin)
|
router.Use(jwtAuthenticatorWebAdmin)
|
||||||
|
|
||||||
router.Get(webLogoutPath, handleWebLogout)
|
router.Get(webLogoutPath, handleWebLogout)
|
||||||
router.With(s.refreshCookie).Get(webChangeAdminPwdPath, handleWebAdminChangePwd)
|
router.With(s.refreshCookie).Get(webAdminCredentialsPath, handleWebAdminCredentials)
|
||||||
router.Post(webChangeAdminPwdPath, handleWebAdminChangePwdPost)
|
router.Post(webChangeAdminPwdPath, handleWebAdminChangePwdPost)
|
||||||
|
router.Post(webChangeAdminAPIKeyAccessPath, handleWebAdminManageAPIKeyPost)
|
||||||
router.With(checkPerm(dataprovider.PermAdminViewUsers), s.refreshCookie).
|
router.With(checkPerm(dataprovider.PermAdminViewUsers), s.refreshCookie).
|
||||||
Get(webUsersPath, handleGetWebUsers)
|
Get(webUsersPath, handleGetWebUsers)
|
||||||
router.With(checkPerm(dataprovider.PermAdminAddUsers), s.refreshCookie).
|
router.With(checkPerm(dataprovider.PermAdminAddUsers), s.refreshCookie).
|
||||||
|
@ -782,14 +812,16 @@ func (s *httpdServer) initializeRouter() {
|
||||||
router.With(checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie).
|
router.With(checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie).
|
||||||
Get(webAdminPath+"/{username}", handleWebUpdateAdminGet)
|
Get(webAdminPath+"/{username}", handleWebUpdateAdminGet)
|
||||||
router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Post(webAdminPath, handleWebAddAdminPost)
|
router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Post(webAdminPath, handleWebAddAdminPost)
|
||||||
router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Post(webAdminPath+"/{username}", handleWebUpdateAdminPost)
|
router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Post(webAdminPath+"/{username}",
|
||||||
|
handleWebUpdateAdminPost)
|
||||||
router.With(checkPerm(dataprovider.PermAdminManageAdmins), verifyCSRFHeader).
|
router.With(checkPerm(dataprovider.PermAdminManageAdmins), verifyCSRFHeader).
|
||||||
Delete(webAdminPath+"/{username}", deleteAdmin)
|
Delete(webAdminPath+"/{username}", deleteAdmin)
|
||||||
router.With(checkPerm(dataprovider.PermAdminCloseConnections), verifyCSRFHeader).
|
router.With(checkPerm(dataprovider.PermAdminCloseConnections), verifyCSRFHeader).
|
||||||
Delete(webConnectionsPath+"/{connectionID}", handleCloseConnection)
|
Delete(webConnectionsPath+"/{connectionID}", handleCloseConnection)
|
||||||
router.With(checkPerm(dataprovider.PermAdminChangeUsers), s.refreshCookie).
|
router.With(checkPerm(dataprovider.PermAdminChangeUsers), s.refreshCookie).
|
||||||
Get(webFolderPath+"/{name}", handleWebUpdateFolderGet)
|
Get(webFolderPath+"/{name}", handleWebUpdateFolderGet)
|
||||||
router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Post(webFolderPath+"/{name}", handleWebUpdateFolderPost)
|
router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Post(webFolderPath+"/{name}",
|
||||||
|
handleWebUpdateFolderPost)
|
||||||
router.With(checkPerm(dataprovider.PermAdminDeleteUsers), verifyCSRFHeader).
|
router.With(checkPerm(dataprovider.PermAdminDeleteUsers), verifyCSRFHeader).
|
||||||
Delete(webFolderPath+"/{name}", deleteFolder)
|
Delete(webFolderPath+"/{name}", deleteFolder)
|
||||||
router.With(checkPerm(dataprovider.PermAdminQuotaScans), verifyCSRFHeader).
|
router.With(checkPerm(dataprovider.PermAdminQuotaScans), verifyCSRFHeader).
|
||||||
|
@ -809,7 +841,8 @@ func (s *httpdServer) initializeRouter() {
|
||||||
router.With(checkPerm(dataprovider.PermAdminManageSystem)).Post(webTemplateFolder, handleWebTemplateFolderPost)
|
router.With(checkPerm(dataprovider.PermAdminManageSystem)).Post(webTemplateFolder, handleWebTemplateFolderPost)
|
||||||
router.With(checkPerm(dataprovider.PermAdminViewDefender)).Get(webDefenderPath, handleWebDefenderPage)
|
router.With(checkPerm(dataprovider.PermAdminViewDefender)).Get(webDefenderPath, handleWebDefenderPage)
|
||||||
router.With(checkPerm(dataprovider.PermAdminViewDefender)).Get(webDefenderHostsPath, getDefenderHosts)
|
router.With(checkPerm(dataprovider.PermAdminViewDefender)).Get(webDefenderHostsPath, getDefenderHosts)
|
||||||
router.With(checkPerm(dataprovider.PermAdminManageDefender)).Delete(webDefenderHostsPath+"/{id}", deleteDefenderHostByID)
|
router.With(checkPerm(dataprovider.PermAdminManageDefender)).Delete(webDefenderHostsPath+"/{id}",
|
||||||
|
deleteDefenderHostByID)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,7 @@ const (
|
||||||
templateStatus = "status.html"
|
templateStatus = "status.html"
|
||||||
templateLogin = "login.html"
|
templateLogin = "login.html"
|
||||||
templateDefender = "defender.html"
|
templateDefender = "defender.html"
|
||||||
templateChangePwd = "changepwd.html"
|
templateCredentials = "credentials.html"
|
||||||
templateMaintenance = "maintenance.html"
|
templateMaintenance = "maintenance.html"
|
||||||
templateSetup = "adminsetup.html"
|
templateSetup = "adminsetup.html"
|
||||||
pageUsersTitle = "Users"
|
pageUsersTitle = "Users"
|
||||||
|
@ -62,7 +62,7 @@ const (
|
||||||
pageConnectionsTitle = "Connections"
|
pageConnectionsTitle = "Connections"
|
||||||
pageStatusTitle = "Status"
|
pageStatusTitle = "Status"
|
||||||
pageFoldersTitle = "Folders"
|
pageFoldersTitle = "Folders"
|
||||||
pageChangePwdTitle = "Change password"
|
pageCredentialsTitle = "Manage credentials"
|
||||||
pageMaintenanceTitle = "Maintenance"
|
pageMaintenanceTitle = "Maintenance"
|
||||||
pageDefenderTitle = "Defender"
|
pageDefenderTitle = "Defender"
|
||||||
pageSetupTitle = "Create first admin user"
|
pageSetupTitle = "Create first admin user"
|
||||||
|
@ -88,7 +88,7 @@ type basePage struct {
|
||||||
FolderTemplateURL string
|
FolderTemplateURL string
|
||||||
DefenderURL string
|
DefenderURL string
|
||||||
LogoutURL string
|
LogoutURL string
|
||||||
ChangeAdminPwdURL string
|
CredentialsURL string
|
||||||
FolderQuotaScanURL string
|
FolderQuotaScanURL string
|
||||||
StatusURL string
|
StatusURL string
|
||||||
MaintenanceURL string
|
MaintenanceURL string
|
||||||
|
@ -153,9 +153,13 @@ type adminPage struct {
|
||||||
IsAdd bool
|
IsAdd bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type changePwdPage struct {
|
type credentialsPage struct {
|
||||||
basePage
|
basePage
|
||||||
Error string
|
Error string
|
||||||
|
AllowAPIKeyAuth bool
|
||||||
|
ChangePwdURL string
|
||||||
|
ManageAPIKeyURL string
|
||||||
|
APIKeyError string
|
||||||
}
|
}
|
||||||
|
|
||||||
type maintenancePage struct {
|
type maintenancePage struct {
|
||||||
|
@ -213,9 +217,9 @@ func loadAdminTemplates(templatesPath string) {
|
||||||
filepath.Join(templatesPath, templateAdminDir, templateBase),
|
filepath.Join(templatesPath, templateAdminDir, templateBase),
|
||||||
filepath.Join(templatesPath, templateAdminDir, templateAdmin),
|
filepath.Join(templatesPath, templateAdminDir, templateAdmin),
|
||||||
}
|
}
|
||||||
changePwdPaths := []string{
|
credentialsPaths := []string{
|
||||||
filepath.Join(templatesPath, templateAdminDir, templateBase),
|
filepath.Join(templatesPath, templateAdminDir, templateBase),
|
||||||
filepath.Join(templatesPath, templateAdminDir, templateChangePwd),
|
filepath.Join(templatesPath, templateAdminDir, templateCredentials),
|
||||||
}
|
}
|
||||||
connectionsPaths := []string{
|
connectionsPaths := []string{
|
||||||
filepath.Join(templatesPath, templateAdminDir, templateBase),
|
filepath.Join(templatesPath, templateAdminDir, templateBase),
|
||||||
|
@ -266,7 +270,7 @@ func loadAdminTemplates(templatesPath string) {
|
||||||
folderTmpl := util.LoadTemplate(rootTpl, folderPath...)
|
folderTmpl := util.LoadTemplate(rootTpl, folderPath...)
|
||||||
statusTmpl := util.LoadTemplate(rootTpl, statusPath...)
|
statusTmpl := util.LoadTemplate(rootTpl, statusPath...)
|
||||||
loginTmpl := util.LoadTemplate(rootTpl, loginPath...)
|
loginTmpl := util.LoadTemplate(rootTpl, loginPath...)
|
||||||
changePwdTmpl := util.LoadTemplate(rootTpl, changePwdPaths...)
|
credentialsTmpl := util.LoadTemplate(rootTpl, credentialsPaths...)
|
||||||
maintenanceTmpl := util.LoadTemplate(rootTpl, maintenancePath...)
|
maintenanceTmpl := util.LoadTemplate(rootTpl, maintenancePath...)
|
||||||
defenderTmpl := util.LoadTemplate(rootTpl, defenderPath...)
|
defenderTmpl := util.LoadTemplate(rootTpl, defenderPath...)
|
||||||
setupTmpl := util.LoadTemplate(rootTpl, setupPath...)
|
setupTmpl := util.LoadTemplate(rootTpl, setupPath...)
|
||||||
|
@ -281,7 +285,7 @@ func loadAdminTemplates(templatesPath string) {
|
||||||
adminTemplates[templateFolder] = folderTmpl
|
adminTemplates[templateFolder] = folderTmpl
|
||||||
adminTemplates[templateStatus] = statusTmpl
|
adminTemplates[templateStatus] = statusTmpl
|
||||||
adminTemplates[templateLogin] = loginTmpl
|
adminTemplates[templateLogin] = loginTmpl
|
||||||
adminTemplates[templateChangePwd] = changePwdTmpl
|
adminTemplates[templateCredentials] = credentialsTmpl
|
||||||
adminTemplates[templateMaintenance] = maintenanceTmpl
|
adminTemplates[templateMaintenance] = maintenanceTmpl
|
||||||
adminTemplates[templateDefender] = defenderTmpl
|
adminTemplates[templateDefender] = defenderTmpl
|
||||||
adminTemplates[templateSetup] = setupTmpl
|
adminTemplates[templateSetup] = setupTmpl
|
||||||
|
@ -305,7 +309,7 @@ func getBasePageData(title, currentURL string, r *http.Request) basePage {
|
||||||
FolderTemplateURL: webTemplateFolder,
|
FolderTemplateURL: webTemplateFolder,
|
||||||
DefenderURL: webDefenderPath,
|
DefenderURL: webDefenderPath,
|
||||||
LogoutURL: webLogoutPath,
|
LogoutURL: webLogoutPath,
|
||||||
ChangeAdminPwdURL: webChangeAdminPwdPath,
|
CredentialsURL: webAdminCredentialsPath,
|
||||||
QuotaScanURL: webQuotaScanPath,
|
QuotaScanURL: webQuotaScanPath,
|
||||||
ConnectionsURL: webConnectionsPath,
|
ConnectionsURL: webConnectionsPath,
|
||||||
StatusURL: webStatusPath,
|
StatusURL: webStatusPath,
|
||||||
|
@ -366,13 +370,22 @@ func renderNotFoundPage(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
renderMessagePage(w, r, page404Title, page404Body, http.StatusNotFound, err, "")
|
renderMessagePage(w, r, page404Title, page404Body, http.StatusNotFound, err, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderChangePwdPage(w http.ResponseWriter, r *http.Request, error string) {
|
func renderCredentialsPage(w http.ResponseWriter, r *http.Request, pwdError, apiKeyError string) {
|
||||||
data := changePwdPage{
|
data := credentialsPage{
|
||||||
basePage: getBasePageData(pageChangePwdTitle, webChangeAdminPwdPath, r),
|
basePage: getBasePageData(pageCredentialsTitle, webAdminCredentialsPath, r),
|
||||||
Error: error,
|
ChangePwdURL: webChangeAdminPwdPath,
|
||||||
|
ManageAPIKeyURL: webChangeAdminAPIKeyAccessPath,
|
||||||
|
Error: pwdError,
|
||||||
|
APIKeyError: apiKeyError,
|
||||||
}
|
}
|
||||||
|
admin, err := dataprovider.AdminExists(data.LoggedAdmin.Username)
|
||||||
|
if err != nil {
|
||||||
|
renderInternalServerErrorPage(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.AllowAPIKeyAuth = admin.Filters.AllowAPIKeyAuth
|
||||||
|
|
||||||
renderAdminTemplate(w, templateChangePwd, data)
|
renderAdminTemplate(w, templateCredentials, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderMaintenancePage(w http.ResponseWriter, r *http.Request, error string) {
|
func renderMaintenancePage(w http.ResponseWriter, r *http.Request, error string) {
|
||||||
|
@ -658,6 +671,7 @@ func getFiltersFromUserPostFields(r *http.Request) sdk.UserFilters {
|
||||||
filters.Hooks.CheckPasswordDisabled = true
|
filters.Hooks.CheckPasswordDisabled = true
|
||||||
}
|
}
|
||||||
filters.DisableFsChecks = len(r.Form.Get("disable_fs_checks")) > 0
|
filters.DisableFsChecks = len(r.Form.Get("disable_fs_checks")) > 0
|
||||||
|
filters.AllowAPIKeyAuth = len(r.Form.Get("allow_api_key_auth")) > 0
|
||||||
return filters
|
return filters
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -820,6 +834,7 @@ func getAdminFromPostFields(r *http.Request) (dataprovider.Admin, error) {
|
||||||
admin.Email = r.Form.Get("email")
|
admin.Email = r.Form.Get("email")
|
||||||
admin.Status = status
|
admin.Status = status
|
||||||
admin.Filters.AllowList = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
|
admin.Filters.AllowList = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
|
||||||
|
admin.Filters.AllowAPIKeyAuth = len(r.Form.Get("allow_api_key_auth")) > 0
|
||||||
admin.AdditionalInfo = r.Form.Get("additional_info")
|
admin.AdditionalInfo = r.Form.Get("additional_info")
|
||||||
admin.Description = r.Form.Get("description")
|
admin.Description = r.Form.Get("description")
|
||||||
return admin, nil
|
return admin, nil
|
||||||
|
@ -1018,15 +1033,16 @@ func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
|
||||||
return user, err
|
return user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleWebAdminChangePwd(w http.ResponseWriter, r *http.Request) {
|
func handleWebAdminCredentials(w http.ResponseWriter, r *http.Request) {
|
||||||
renderChangePwdPage(w, r, "")
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
renderCredentialsPage(w, r, "", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleWebAdminChangePwdPost(w http.ResponseWriter, r *http.Request) {
|
func handleWebAdminChangePwdPost(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
err := r.ParseForm()
|
err := r.ParseForm()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderChangePwdPage(w, r, err.Error())
|
renderCredentialsPage(w, r, err.Error(), "")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
|
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
|
||||||
|
@ -1036,13 +1052,45 @@ func handleWebAdminChangePwdPost(w http.ResponseWriter, r *http.Request) {
|
||||||
err = doChangeAdminPassword(r, r.Form.Get("current_password"), r.Form.Get("new_password1"),
|
err = doChangeAdminPassword(r, r.Form.Get("current_password"), r.Form.Get("new_password1"),
|
||||||
r.Form.Get("new_password2"))
|
r.Form.Get("new_password2"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderChangePwdPage(w, r, err.Error())
|
renderCredentialsPage(w, r, err.Error(), "")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
handleWebLogout(w, r)
|
handleWebLogout(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleWebAdminManageAPIKeyPost(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
renderCredentialsPage(w, r, err.Error(), "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
|
||||||
|
renderForbiddenPage(w, r, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
renderCredentialsPage(w, r, "", "Invalid token claims")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
admin, err := dataprovider.AdminExists(claims.Username)
|
||||||
|
if err != nil {
|
||||||
|
renderCredentialsPage(w, r, "", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
admin.Filters.AllowAPIKeyAuth = len(r.Form.Get("allow_api_key_auth")) > 0
|
||||||
|
err = dataprovider.UpdateAdmin(&admin)
|
||||||
|
if err != nil {
|
||||||
|
renderCredentialsPage(w, r, "", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
renderMessagePage(w, r, "API key authentication updated", "", http.StatusOK, nil,
|
||||||
|
"Your API key access permission has been successfully updated")
|
||||||
|
}
|
||||||
|
|
||||||
func handleWebLogout(w http.ResponseWriter, r *http.Request) {
|
func handleWebLogout(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
c := jwtTokenClaims{}
|
c := jwtTokenClaims{}
|
||||||
c.removeCookie(w, r, webBaseAdminPath)
|
c.removeCookie(w, r, webBaseAdminPath)
|
||||||
|
|
||||||
|
@ -1050,10 +1098,12 @@ func handleWebLogout(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleWebMaintenance(w http.ResponseWriter, r *http.Request) {
|
func handleWebMaintenance(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
renderMaintenancePage(w, r, "")
|
renderMaintenancePage(w, r, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleWebRestore(w http.ResponseWriter, r *http.Request) {
|
func handleWebRestore(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, MaxRestoreSize)
|
||||||
err := r.ParseMultipartForm(MaxRestoreSize)
|
err := r.ParseMultipartForm(MaxRestoreSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderMaintenancePage(w, r, err.Error())
|
renderMaintenancePage(w, r, err.Error())
|
||||||
|
@ -1098,6 +1148,7 @@ func handleWebRestore(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleGetWebAdmins(w http.ResponseWriter, r *http.Request) {
|
func handleGetWebAdmins(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
limit := defaultQueryLimit
|
limit := defaultQueryLimit
|
||||||
if _, ok := r.URL.Query()["qlimit"]; ok {
|
if _, ok := r.URL.Query()["qlimit"]; ok {
|
||||||
var err error
|
var err error
|
||||||
|
@ -1126,6 +1177,7 @@ func handleGetWebAdmins(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleWebAdminSetupGet(w http.ResponseWriter, r *http.Request) {
|
func handleWebAdminSetupGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
|
||||||
if dataprovider.HasAdmin() {
|
if dataprovider.HasAdmin() {
|
||||||
http.Redirect(w, r, webLoginPath, http.StatusFound)
|
http.Redirect(w, r, webLoginPath, http.StatusFound)
|
||||||
return
|
return
|
||||||
|
@ -1134,11 +1186,13 @@ func handleWebAdminSetupGet(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleWebAddAdminGet(w http.ResponseWriter, r *http.Request) {
|
func handleWebAddAdminGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
admin := &dataprovider.Admin{Status: 1}
|
admin := &dataprovider.Admin{Status: 1}
|
||||||
renderAddUpdateAdminPage(w, r, admin, "", true)
|
renderAddUpdateAdminPage(w, r, admin, "", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleWebUpdateAdminGet(w http.ResponseWriter, r *http.Request) {
|
func handleWebUpdateAdminGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
username := getURLParam(r, "username")
|
username := getURLParam(r, "username")
|
||||||
admin, err := dataprovider.AdminExists(username)
|
admin, err := dataprovider.AdminExists(username)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
@ -1220,6 +1274,7 @@ func handleWebUpdateAdminPost(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleWebDefenderPage(w http.ResponseWriter, r *http.Request) {
|
func handleWebDefenderPage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
data := defenderHostsPage{
|
data := defenderHostsPage{
|
||||||
basePage: getBasePageData(pageDefenderTitle, webDefenderPath, r),
|
basePage: getBasePageData(pageDefenderTitle, webDefenderPath, r),
|
||||||
DefenderHostsURL: webDefenderHostsPath,
|
DefenderHostsURL: webDefenderHostsPath,
|
||||||
|
@ -1229,6 +1284,7 @@ func handleWebDefenderPage(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleGetWebUsers(w http.ResponseWriter, r *http.Request) {
|
func handleGetWebUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
limit := defaultQueryLimit
|
limit := defaultQueryLimit
|
||||||
if _, ok := r.URL.Query()["qlimit"]; ok {
|
if _, ok := r.URL.Query()["qlimit"]; ok {
|
||||||
var err error
|
var err error
|
||||||
|
@ -1257,6 +1313,7 @@ func handleGetWebUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleWebTemplateFolderGet(w http.ResponseWriter, r *http.Request) {
|
func handleWebTemplateFolderGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
if r.URL.Query().Get("from") != "" {
|
if r.URL.Query().Get("from") != "" {
|
||||||
name := r.URL.Query().Get("from")
|
name := r.URL.Query().Get("from")
|
||||||
folder, err := dataprovider.GetFolderByName(name)
|
folder, err := dataprovider.GetFolderByName(name)
|
||||||
|
@ -1319,6 +1376,7 @@ func handleWebTemplateFolderPost(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleWebTemplateUserGet(w http.ResponseWriter, r *http.Request) {
|
func handleWebTemplateUserGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
if r.URL.Query().Get("from") != "" {
|
if r.URL.Query().Get("from") != "" {
|
||||||
username := r.URL.Query().Get("from")
|
username := r.URL.Query().Get("from")
|
||||||
user, err := dataprovider.UserExists(username)
|
user, err := dataprovider.UserExists(username)
|
||||||
|
@ -1376,6 +1434,7 @@ func handleWebTemplateUserPost(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleWebAddUserGet(w http.ResponseWriter, r *http.Request) {
|
func handleWebAddUserGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
if r.URL.Query().Get("clone-from") != "" {
|
if r.URL.Query().Get("clone-from") != "" {
|
||||||
username := r.URL.Query().Get("clone-from")
|
username := r.URL.Query().Get("clone-from")
|
||||||
user, err := dataprovider.UserExists(username)
|
user, err := dataprovider.UserExists(username)
|
||||||
|
@ -1397,6 +1456,7 @@ func handleWebAddUserGet(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleWebUpdateUserGet(w http.ResponseWriter, r *http.Request) {
|
func handleWebUpdateUserGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
username := getURLParam(r, "username")
|
username := getURLParam(r, "username")
|
||||||
user, err := dataprovider.UserExists(username)
|
user, err := dataprovider.UserExists(username)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
@ -1469,6 +1529,7 @@ func handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleWebGetStatus(w http.ResponseWriter, r *http.Request) {
|
func handleWebGetStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
data := statusPage{
|
data := statusPage{
|
||||||
basePage: getBasePageData(pageStatusTitle, webStatusPath, r),
|
basePage: getBasePageData(pageStatusTitle, webStatusPath, r),
|
||||||
Status: getServicesStatus(),
|
Status: getServicesStatus(),
|
||||||
|
@ -1477,6 +1538,7 @@ func handleWebGetStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleWebGetConnections(w http.ResponseWriter, r *http.Request) {
|
func handleWebGetConnections(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
connectionStats := common.Connections.GetStats()
|
connectionStats := common.Connections.GetStats()
|
||||||
data := connectionsPage{
|
data := connectionsPage{
|
||||||
basePage: getBasePageData(pageConnectionsTitle, webConnectionsPath, r),
|
basePage: getBasePageData(pageConnectionsTitle, webConnectionsPath, r),
|
||||||
|
@ -1486,6 +1548,7 @@ func handleWebGetConnections(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleWebAddFolderGet(w http.ResponseWriter, r *http.Request) {
|
func handleWebAddFolderGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
renderFolderPage(w, r, vfs.BaseVirtualFolder{}, folderPageModeAdd, "")
|
renderFolderPage(w, r, vfs.BaseVirtualFolder{}, folderPageModeAdd, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1520,6 +1583,7 @@ func handleWebAddFolderPost(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleWebUpdateFolderGet(w http.ResponseWriter, r *http.Request) {
|
func handleWebUpdateFolderGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
name := getURLParam(r, "name")
|
name := getURLParam(r, "name")
|
||||||
folder, err := dataprovider.GetFolderByName(name)
|
folder, err := dataprovider.GetFolderByName(name)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
@ -1594,6 +1658,7 @@ func getWebVirtualFolders(w http.ResponseWriter, r *http.Request, limit int) ([]
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleWebGetFolders(w http.ResponseWriter, r *http.Request) {
|
func handleWebGetFolders(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
limit := defaultQueryLimit
|
limit := defaultQueryLimit
|
||||||
if _, ok := r.URL.Query()["qlimit"]; ok {
|
if _, ok := r.URL.Query()["qlimit"]; ok {
|
||||||
var err error
|
var err error
|
||||||
|
|
|
@ -91,13 +91,16 @@ type clientMessagePage struct {
|
||||||
Success string
|
Success string
|
||||||
}
|
}
|
||||||
|
|
||||||
type credentialsPage struct {
|
type clientCredentialsPage struct {
|
||||||
baseClientPage
|
baseClientPage
|
||||||
PublicKeys []string
|
PublicKeys []string
|
||||||
ChangePwdURL string
|
AllowAPIKeyAuth bool
|
||||||
ManageKeysURL string
|
ChangePwdURL string
|
||||||
PwdError string
|
ManageKeysURL string
|
||||||
KeyError string
|
ManageAPIKeyURL string
|
||||||
|
PwdError string
|
||||||
|
KeyError string
|
||||||
|
APIKeyError string
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFileObjectURL(baseDir, name string) string {
|
func getFileObjectURL(baseDir, name string) string {
|
||||||
|
@ -235,23 +238,28 @@ func renderFilesPage(w http.ResponseWriter, r *http.Request, dirName, error stri
|
||||||
renderClientTemplate(w, templateClientFiles, data)
|
renderClientTemplate(w, templateClientFiles, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderCredentialsPage(w http.ResponseWriter, r *http.Request, pwdError string, keyError string) {
|
func renderClientCredentialsPage(w http.ResponseWriter, r *http.Request, pwdError, keyError, apiKeyError string) {
|
||||||
data := credentialsPage{
|
data := clientCredentialsPage{
|
||||||
baseClientPage: getBaseClientPageData(pageClientCredentialsTitle, webClientCredentialsPath, r),
|
baseClientPage: getBaseClientPageData(pageClientCredentialsTitle, webClientCredentialsPath, r),
|
||||||
ChangePwdURL: webChangeClientPwdPath,
|
ChangePwdURL: webChangeClientPwdPath,
|
||||||
ManageKeysURL: webChangeClientKeysPath,
|
ManageKeysURL: webChangeClientKeysPath,
|
||||||
PwdError: pwdError,
|
ManageAPIKeyURL: webChangeClientAPIKeyAccessPath,
|
||||||
KeyError: keyError,
|
PwdError: pwdError,
|
||||||
|
KeyError: keyError,
|
||||||
|
APIKeyError: apiKeyError,
|
||||||
}
|
}
|
||||||
user, err := dataprovider.UserExists(data.LoggedUser.Username)
|
user, err := dataprovider.UserExists(data.LoggedUser.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderClientInternalServerErrorPage(w, r, err)
|
renderClientInternalServerErrorPage(w, r, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
data.PublicKeys = user.PublicKeys
|
data.PublicKeys = user.PublicKeys
|
||||||
|
data.AllowAPIKeyAuth = user.Filters.AllowAPIKeyAuth
|
||||||
renderClientTemplate(w, templateClientCredentials, data)
|
renderClientTemplate(w, templateClientCredentials, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleWebClientLogout(w http.ResponseWriter, r *http.Request) {
|
func handleWebClientLogout(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
|
||||||
c := jwtTokenClaims{}
|
c := jwtTokenClaims{}
|
||||||
c.removeCookie(w, r, webBaseClientPath)
|
c.removeCookie(w, r, webBaseClientPath)
|
||||||
|
|
||||||
|
@ -259,6 +267,7 @@ func handleWebClientLogout(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleWebClientDownloadZip(w http.ResponseWriter, r *http.Request) {
|
func handleWebClientDownloadZip(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
claims, err := getTokenClaims(r)
|
claims, err := getTokenClaims(r)
|
||||||
if err != nil || claims.Username == "" {
|
if err != nil || claims.Username == "" {
|
||||||
renderClientMessagePage(w, r, "Invalid token claims", "", http.StatusForbidden, nil, "")
|
renderClientMessagePage(w, r, "Invalid token claims", "", http.StatusForbidden, nil, "")
|
||||||
|
@ -303,6 +312,7 @@ func handleWebClientDownloadZip(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleClientGetDirContents(w http.ResponseWriter, r *http.Request) {
|
func handleClientGetDirContents(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
claims, err := getTokenClaims(r)
|
claims, err := getTokenClaims(r)
|
||||||
if err != nil || claims.Username == "" {
|
if err != nil || claims.Username == "" {
|
||||||
sendAPIResponse(w, r, nil, "invalid token claims", http.StatusForbidden)
|
sendAPIResponse(w, r, nil, "invalid token claims", http.StatusForbidden)
|
||||||
|
@ -365,6 +375,7 @@ func handleClientGetDirContents(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleClientGetFiles(w http.ResponseWriter, r *http.Request) {
|
func handleClientGetFiles(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
claims, err := getTokenClaims(r)
|
claims, err := getTokenClaims(r)
|
||||||
if err != nil || claims.Username == "" {
|
if err != nil || claims.Username == "" {
|
||||||
renderClientForbiddenPage(w, r, "Invalid token claims")
|
renderClientForbiddenPage(w, r, "Invalid token claims")
|
||||||
|
@ -421,14 +432,15 @@ func handleClientGetFiles(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleClientGetCredentials(w http.ResponseWriter, r *http.Request) {
|
func handleClientGetCredentials(w http.ResponseWriter, r *http.Request) {
|
||||||
renderCredentialsPage(w, r, "", "")
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
renderClientCredentialsPage(w, r, "", "", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleWebClientChangePwdPost(w http.ResponseWriter, r *http.Request) {
|
func handleWebClientChangePwdPost(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
err := r.ParseForm()
|
err := r.ParseForm()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderCredentialsPage(w, r, err.Error(), "")
|
renderClientCredentialsPage(w, r, err.Error(), "", "")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
|
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
|
||||||
|
@ -438,7 +450,7 @@ func handleWebClientChangePwdPost(w http.ResponseWriter, r *http.Request) {
|
||||||
err = doChangeUserPassword(r, r.Form.Get("current_password"), r.Form.Get("new_password1"),
|
err = doChangeUserPassword(r, r.Form.Get("current_password"), r.Form.Get("new_password1"),
|
||||||
r.Form.Get("new_password2"))
|
r.Form.Get("new_password2"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderCredentialsPage(w, r, err.Error(), "")
|
renderClientCredentialsPage(w, r, err.Error(), "", "")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
handleWebClientLogout(w, r)
|
handleWebClientLogout(w, r)
|
||||||
|
@ -448,7 +460,7 @@ func handleWebClientManageKeysPost(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
err := r.ParseForm()
|
err := r.ParseForm()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderCredentialsPage(w, r, "", err.Error())
|
renderClientCredentialsPage(w, r, "", err.Error(), "")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
|
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
|
||||||
|
@ -457,19 +469,51 @@ func handleWebClientManageKeysPost(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
claims, err := getTokenClaims(r)
|
claims, err := getTokenClaims(r)
|
||||||
if err != nil || claims.Username == "" {
|
if err != nil || claims.Username == "" {
|
||||||
renderCredentialsPage(w, r, "", "Invalid token claims")
|
renderClientCredentialsPage(w, r, "", "Invalid token claims", "")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user, err := dataprovider.UserExists(claims.Username)
|
user, err := dataprovider.UserExists(claims.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderCredentialsPage(w, r, "", err.Error())
|
renderClientCredentialsPage(w, r, "", err.Error(), "")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user.PublicKeys = r.Form["public_keys"]
|
user.PublicKeys = r.Form["public_keys"]
|
||||||
err = dataprovider.UpdateUser(&user)
|
err = dataprovider.UpdateUser(&user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderCredentialsPage(w, r, "", err.Error())
|
renderClientCredentialsPage(w, r, "", err.Error(), "")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
renderClientMessagePage(w, r, "Public keys updated", "", http.StatusOK, nil, "Your public keys has been successfully updated")
|
renderClientMessagePage(w, r, "Public keys updated", "", http.StatusOK, nil,
|
||||||
|
"Your public keys has been successfully updated")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleWebClientManageAPIKeyPost(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
renderClientCredentialsPage(w, r, "", "", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
|
||||||
|
renderClientForbiddenPage(w, r, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
renderClientCredentialsPage(w, r, "", "", "Invalid token claims")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user, err := dataprovider.UserExists(claims.Username)
|
||||||
|
if err != nil {
|
||||||
|
renderClientCredentialsPage(w, r, "", "", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.Filters.AllowAPIKeyAuth = len(r.Form.Get("allow_api_key_auth")) > 0
|
||||||
|
err = dataprovider.UpdateUser(&user)
|
||||||
|
if err != nil {
|
||||||
|
renderClientCredentialsPage(w, r, "", "", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
renderClientMessagePage(w, r, "API key authentication updated", "", http.StatusOK, nil,
|
||||||
|
"Your API key access permission has been successfully updated")
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,7 @@ const (
|
||||||
defenderScore = "/api/v2/defender/score"
|
defenderScore = "/api/v2/defender/score"
|
||||||
adminPath = "/api/v2/admins"
|
adminPath = "/api/v2/admins"
|
||||||
adminPwdPath = "/api/v2/admin/changepwd"
|
adminPwdPath = "/api/v2/admin/changepwd"
|
||||||
|
apiKeysPath = "/api/v2/apikeys"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -241,7 +242,7 @@ func GetUsers(limit, offset int64, expectedStatusCode int) ([]dataprovider.User,
|
||||||
return users, body, err
|
return users, body, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddAdmin adds a new user and checks the received HTTP Status code against expectedStatusCode.
|
// AddAdmin adds a new admin and checks the received HTTP Status code against expectedStatusCode.
|
||||||
func AddAdmin(admin dataprovider.Admin, expectedStatusCode int) (dataprovider.Admin, []byte, error) {
|
func AddAdmin(admin dataprovider.Admin, expectedStatusCode int) (dataprovider.Admin, []byte, error) {
|
||||||
var newAdmin dataprovider.Admin
|
var newAdmin dataprovider.Admin
|
||||||
var body []byte
|
var body []byte
|
||||||
|
@ -372,6 +373,121 @@ func ChangeAdminPassword(currentPassword, newPassword string, expectedStatusCode
|
||||||
return body, err
|
return body, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAPIKeys returns a list of API keys and checks the received HTTP Status code against expectedStatusCode.
|
||||||
|
// The number of results can be limited specifying a limit.
|
||||||
|
// Some results can be skipped specifying an offset.
|
||||||
|
func GetAPIKeys(limit, offset int64, expectedStatusCode int) ([]dataprovider.APIKey, []byte, error) {
|
||||||
|
var apiKeys []dataprovider.APIKey
|
||||||
|
var body []byte
|
||||||
|
url, err := addLimitAndOffsetQueryParams(buildURLRelativeToBase(apiKeysPath), limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
return apiKeys, body, err
|
||||||
|
}
|
||||||
|
resp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, "", getDefaultToken())
|
||||||
|
if err != nil {
|
||||||
|
return apiKeys, body, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
err = checkResponse(resp.StatusCode, expectedStatusCode)
|
||||||
|
if err == nil && expectedStatusCode == http.StatusOK {
|
||||||
|
err = render.DecodeJSON(resp.Body, &apiKeys)
|
||||||
|
} else {
|
||||||
|
body, _ = getResponseBody(resp)
|
||||||
|
}
|
||||||
|
return apiKeys, body, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddAPIKey adds a new API key and checks the received HTTP Status code against expectedStatusCode.
|
||||||
|
func AddAPIKey(apiKey dataprovider.APIKey, expectedStatusCode int) (dataprovider.APIKey, []byte, error) {
|
||||||
|
var newAPIKey dataprovider.APIKey
|
||||||
|
var body []byte
|
||||||
|
asJSON, _ := json.Marshal(apiKey)
|
||||||
|
resp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(apiKeysPath), bytes.NewBuffer(asJSON),
|
||||||
|
"application/json", getDefaultToken())
|
||||||
|
if err != nil {
|
||||||
|
return newAPIKey, body, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
err = checkResponse(resp.StatusCode, expectedStatusCode)
|
||||||
|
if expectedStatusCode != http.StatusCreated {
|
||||||
|
body, _ = getResponseBody(resp)
|
||||||
|
return newAPIKey, body, err
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
body, _ = getResponseBody(resp)
|
||||||
|
return newAPIKey, body, err
|
||||||
|
}
|
||||||
|
response := make(map[string]string)
|
||||||
|
err = render.DecodeJSON(resp.Body, &response)
|
||||||
|
if err == nil {
|
||||||
|
newAPIKey, body, err = GetAPIKeyByID(resp.Header.Get("X-Object-ID"), http.StatusOK)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
err = checkAPIKey(&apiKey, &newAPIKey)
|
||||||
|
}
|
||||||
|
newAPIKey.Key = response["key"]
|
||||||
|
|
||||||
|
return newAPIKey, body, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAPIKey updates an existing API key and checks the received HTTP Status code against expectedStatusCode
|
||||||
|
func UpdateAPIKey(apiKey dataprovider.APIKey, expectedStatusCode int) (dataprovider.APIKey, []byte, error) {
|
||||||
|
var newAPIKey dataprovider.APIKey
|
||||||
|
var body []byte
|
||||||
|
|
||||||
|
asJSON, _ := json.Marshal(apiKey)
|
||||||
|
resp, err := sendHTTPRequest(http.MethodPut, buildURLRelativeToBase(apiKeysPath, url.PathEscape(apiKey.KeyID)),
|
||||||
|
bytes.NewBuffer(asJSON), "application/json", getDefaultToken())
|
||||||
|
if err != nil {
|
||||||
|
return newAPIKey, body, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, _ = getResponseBody(resp)
|
||||||
|
err = checkResponse(resp.StatusCode, expectedStatusCode)
|
||||||
|
if expectedStatusCode != http.StatusOK {
|
||||||
|
return newAPIKey, body, err
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
newAPIKey, body, err = GetAPIKeyByID(apiKey.KeyID, expectedStatusCode)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
err = checkAPIKey(&apiKey, &newAPIKey)
|
||||||
|
}
|
||||||
|
return newAPIKey, body, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveAPIKey removes an existing API key and checks the received HTTP Status code against expectedStatusCode.
|
||||||
|
func RemoveAPIKey(apiKey dataprovider.APIKey, expectedStatusCode int) ([]byte, error) {
|
||||||
|
var body []byte
|
||||||
|
resp, err := sendHTTPRequest(http.MethodDelete, buildURLRelativeToBase(apiKeysPath, url.PathEscape(apiKey.KeyID)),
|
||||||
|
nil, "", getDefaultToken())
|
||||||
|
if err != nil {
|
||||||
|
return body, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, _ = getResponseBody(resp)
|
||||||
|
return body, checkResponse(resp.StatusCode, expectedStatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAPIKeyByID gets a API key by ID and checks the received HTTP Status code against expectedStatusCode.
|
||||||
|
func GetAPIKeyByID(keyID string, expectedStatusCode int) (dataprovider.APIKey, []byte, error) {
|
||||||
|
var apiKey dataprovider.APIKey
|
||||||
|
var body []byte
|
||||||
|
resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(apiKeysPath, url.PathEscape(keyID)),
|
||||||
|
nil, "", getDefaultToken())
|
||||||
|
if err != nil {
|
||||||
|
return apiKey, body, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
err = checkResponse(resp.StatusCode, expectedStatusCode)
|
||||||
|
if err == nil && expectedStatusCode == http.StatusOK {
|
||||||
|
err = render.DecodeJSON(resp.Body, &apiKey)
|
||||||
|
} else {
|
||||||
|
body, _ = getResponseBody(resp)
|
||||||
|
}
|
||||||
|
return apiKey, body, err
|
||||||
|
}
|
||||||
|
|
||||||
// GetQuotaScans gets active quota scans for users and checks the received HTTP Status code against expectedStatusCode.
|
// GetQuotaScans gets active quota scans for users and checks the received HTTP Status code against expectedStatusCode.
|
||||||
func GetQuotaScans(expectedStatusCode int) ([]common.ActiveQuotaScan, []byte, error) {
|
func GetQuotaScans(expectedStatusCode int) ([]common.ActiveQuotaScan, []byte, error) {
|
||||||
var quotaScans []common.ActiveQuotaScan
|
var quotaScans []common.ActiveQuotaScan
|
||||||
|
@ -894,7 +1010,42 @@ func checkFolder(expected *vfs.BaseVirtualFolder, actual *vfs.BaseVirtualFolder)
|
||||||
return compareFsConfig(&expected.FsConfig, &actual.FsConfig)
|
return compareFsConfig(&expected.FsConfig, &actual.FsConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkAdmin(expected *dataprovider.Admin, actual *dataprovider.Admin) error {
|
func checkAPIKey(expected, actual *dataprovider.APIKey) error {
|
||||||
|
if actual.Key != "" {
|
||||||
|
return errors.New("key must not be visible")
|
||||||
|
}
|
||||||
|
if actual.KeyID == "" {
|
||||||
|
return errors.New("actual key_id cannot be empty")
|
||||||
|
}
|
||||||
|
if expected.Name != actual.Name {
|
||||||
|
return errors.New("name mismatch")
|
||||||
|
}
|
||||||
|
if expected.Scope != actual.Scope {
|
||||||
|
return errors.New("scope mismatch")
|
||||||
|
}
|
||||||
|
if actual.CreatedAt == 0 {
|
||||||
|
return errors.New("created_at cannot be 0")
|
||||||
|
}
|
||||||
|
if actual.UpdatedAt == 0 {
|
||||||
|
return errors.New("updated_at cannot be 0")
|
||||||
|
}
|
||||||
|
if expected.ExpiresAt != actual.ExpiresAt {
|
||||||
|
return errors.New("expires_at mismatch")
|
||||||
|
}
|
||||||
|
if expected.Description != actual.Description {
|
||||||
|
return errors.New("description mismatch")
|
||||||
|
}
|
||||||
|
if expected.User != actual.User {
|
||||||
|
return errors.New("user mismatch")
|
||||||
|
}
|
||||||
|
if expected.Admin != actual.Admin {
|
||||||
|
return errors.New("admin mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkAdmin(expected, actual *dataprovider.Admin) error {
|
||||||
if actual.Password != "" {
|
if actual.Password != "" {
|
||||||
return errors.New("admin password must not be visible")
|
return errors.New("admin password must not be visible")
|
||||||
}
|
}
|
||||||
|
@ -921,6 +1072,9 @@ func checkAdmin(expected *dataprovider.Admin, actual *dataprovider.Admin) error
|
||||||
if len(expected.Filters.AllowList) != len(actual.Filters.AllowList) {
|
if len(expected.Filters.AllowList) != len(actual.Filters.AllowList) {
|
||||||
return errors.New("allow list mismatch")
|
return errors.New("allow list mismatch")
|
||||||
}
|
}
|
||||||
|
if expected.Filters.AllowAPIKeyAuth != actual.Filters.AllowAPIKeyAuth {
|
||||||
|
return errors.New("allow_api_key_auth mismatch")
|
||||||
|
}
|
||||||
for _, v := range expected.Filters.AllowList {
|
for _, v := range expected.Filters.AllowList {
|
||||||
if !util.IsStringInSlice(v, actual.Filters.AllowList) {
|
if !util.IsStringInSlice(v, actual.Filters.AllowList) {
|
||||||
return errors.New("allow list content mismatch")
|
return errors.New("allow list content mismatch")
|
||||||
|
@ -1270,6 +1424,9 @@ func compareUserFilters(expected *dataprovider.User, actual *dataprovider.User)
|
||||||
if len(expected.Filters.WebClient) != len(actual.Filters.WebClient) {
|
if len(expected.Filters.WebClient) != len(actual.Filters.WebClient) {
|
||||||
return errors.New("WebClient filter mismatch")
|
return errors.New("WebClient filter mismatch")
|
||||||
}
|
}
|
||||||
|
if expected.Filters.AllowAPIKeyAuth != actual.Filters.AllowAPIKeyAuth {
|
||||||
|
return errors.New("allow_api_key_auth mismatch")
|
||||||
|
}
|
||||||
if err := compareUserFilterSubStructs(expected, actual); err != nil {
|
if err := compareUserFilterSubStructs(expected, actual); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,6 +120,8 @@ type UserFilters struct {
|
||||||
DisableFsChecks bool `json:"disable_fs_checks,omitempty"`
|
DisableFsChecks bool `json:"disable_fs_checks,omitempty"`
|
||||||
// WebClient related configuration options
|
// WebClient related configuration options
|
||||||
WebClient []string `json:"web_client,omitempty"`
|
WebClient []string `json:"web_client,omitempty"`
|
||||||
|
// API key auth allows to impersonate this user with an API key
|
||||||
|
AllowAPIKeyAuth bool `json:"allow_api_key_auth,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BaseUser struct {
|
type BaseUser struct {
|
||||||
|
|
|
@ -286,7 +286,11 @@ func (s *Service) loadInitialData() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) restoreDump(dump *dataprovider.BackupData) error {
|
func (s *Service) restoreDump(dump *dataprovider.BackupData) error {
|
||||||
err := httpd.RestoreAdmins(dump.Admins, s.LoadDataFrom, s.LoadDataMode)
|
err := httpd.RestoreAPIKeys(dump.APIKeys, s.LoadDataFrom, s.LoadDataMode)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to restore API keys from file %#v: %v", s.LoadDataFrom, err)
|
||||||
|
}
|
||||||
|
err = httpd.RestoreAdmins(dump.Admins, s.LoadDataFrom, s.LoadDataMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to restore admins from file %#v: %v", s.LoadDataFrom, err)
|
return fmt.Errorf("unable to restore admins from file %#v: %v", s.LoadDataFrom, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,6 +88,17 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="idAllowAPIKeyAuth" name="allow_api_key_auth"
|
||||||
|
{{if .Admin.Filters.AllowAPIKeyAuth}}checked{{end}} aria-describedby="allowAPIKeyAuthHelpBlock">
|
||||||
|
<label for="idAllowAPIKeyAuth" class="form-check-label">Allow API key authentication</label>
|
||||||
|
<small id="allowAPIKeyAuthHelpBlock" class="form-text text-muted">
|
||||||
|
Allow to impersonate this admin, in REST API, with an API key
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="idAdditionalInfo" class="col-sm-2 col-form-label">Additional info</label>
|
<label for="idAdditionalInfo" class="col-sm-2 col-form-label">Additional info</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
|
|
|
@ -69,7 +69,7 @@
|
||||||
<div class="sidebar-brand-icon">
|
<div class="sidebar-brand-icon">
|
||||||
<i class="fas fa-folder-open"></i>
|
<i class="fas fa-folder-open"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-brand-text mx-3" style="text-transform: none;">SFTPGo Web</div>
|
<div class="sidebar-brand-text mx-3" style="text-transform: none;">SFTPGo Admin</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- Divider -->
|
<!-- Divider -->
|
||||||
|
@ -167,9 +167,9 @@
|
||||||
</a>
|
</a>
|
||||||
<!-- Dropdown - User Information -->
|
<!-- Dropdown - User Information -->
|
||||||
<div class="dropdown-menu dropdown-menu-right shadow animated--grow-in" aria-labelledby="userDropdown">
|
<div class="dropdown-menu dropdown-menu-right shadow animated--grow-in" aria-labelledby="userDropdown">
|
||||||
<a class="dropdown-item" href="{{.ChangeAdminPwdURL}}">
|
<a class="dropdown-item" href="{{.CredentialsURL}}">
|
||||||
<i class="fas fa-key fa-sm fa-fw mr-2 text-gray-400"></i>
|
<i class="fas fa-key fa-sm fa-fw mr-2 text-gray-400"></i>
|
||||||
Change password
|
Credentials
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
<a class="dropdown-item" href="#" data-toggle="modal" data-target="#logoutModal">
|
<a class="dropdown-item" href="#" data-toggle="modal" data-target="#logoutModal">
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<div class="card-body text-form-error">{{.Error}}</div>
|
<div class="card-body text-form-error">{{.Error}}</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
<form id="user_form" action="{{.CurrentURL}}" method="POST" autocomplete="off">
|
<form id="user_form" action="{{.ChangePwdURL}}" method="POST" autocomplete="off">
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="idCurrentPassword" class="col-sm-2 col-form-label">Current password</label>
|
<label for="idCurrentPassword" class="col-sm-2 col-form-label">Current password</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
|
@ -41,4 +41,31 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header py-3">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary">REST API access</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{{if .APIKeyError}}
|
||||||
|
<div class="card mb-4 border-left-warning">
|
||||||
|
<div class="card-body text-form-error">{{.APIKeyError}}</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<form id="key_form" action="{{.ManageAPIKeyURL}}" method="POST">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="idAllowAPIKeyAuth" name="allow_api_key_auth"
|
||||||
|
{{if .AllowAPIKeyAuth}}checked{{end}} aria-describedby="allowAPIKeyAuthHelpBlock">
|
||||||
|
<label for="idAllowAPIKeyAuth" class="form-check-label">Allow API key authentication</label>
|
||||||
|
<small id="allowAPIKeyAuthHelpBlock" class="form-text text-muted">
|
||||||
|
Allow to impersonate yourself, in REST API, with an API key. If this permission is not granted, your credentials are required to use the REST API on your behalf
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
|
||||||
|
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Submit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
|
@ -455,6 +455,17 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="idAllowAPIKeyAuth" name="allow_api_key_auth"
|
||||||
|
{{if .User.Filters.AllowAPIKeyAuth}}checked{{end}} aria-describedby="allowAPIKeyAuthHelpBlock">
|
||||||
|
<label for="idAllowAPIKeyAuth" class="form-check-label">Allow API key authentication</label>
|
||||||
|
<small id="allowAPIKeyAuthHelpBlock" class="form-text text-muted">
|
||||||
|
Allow to impersonate this user, in REST API, with an API key
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="idDeniedIP" class="col-sm-2 col-form-label">Denied IP/Mask</label>
|
<label for="idDeniedIP" class="col-sm-2 col-form-label">Denied IP/Mask</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
|
|
|
@ -95,6 +95,33 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header py-3">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary">REST API access</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{{if .APIKeyError}}
|
||||||
|
<div class="card mb-4 border-left-warning">
|
||||||
|
<div class="card-body text-form-error">{{.APIKeyError}}</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<form id="key_form" action="{{.ManageAPIKeyURL}}" method="POST">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="idAllowAPIKeyAuth" name="allow_api_key_auth"
|
||||||
|
{{if .AllowAPIKeyAuth}}checked{{end}} aria-describedby="allowAPIKeyAuthHelpBlock">
|
||||||
|
<label for="idAllowAPIKeyAuth" class="form-check-label">Allow API key authentication</label>
|
||||||
|
<small id="allowAPIKeyAuthHelpBlock" class="form-text text-muted">
|
||||||
|
Allow to impersonate yourself, in REST API, with an API key. If this permission is not granted, your credentials are required to use the REST API on your behalf
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
|
||||||
|
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Submit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "extra_js"}}
|
{{define "extra_js"}}
|
||||||
|
|
Loading…
Reference in a new issue