improve defender and quotas REST API

This commit is contained in:
Nicola Murino 2021-06-07 21:52:43 +02:00
parent 43182fc25e
commit feec2118bb
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
16 changed files with 1068 additions and 259 deletions

View file

@ -199,13 +199,31 @@ func GetDefenderBanTime(ip string) *time.Time {
return Config.defender.GetBanTime(ip)
}
// Unban removes the specified IP address from the banned ones
func Unban(ip string) bool {
// GetDefenderHosts returns hosts that are banned or for which some violations have been detected
func GetDefenderHosts() []*DefenderEntry {
if Config.defender == nil {
return nil
}
return Config.defender.GetHosts()
}
// GetDefenderHost returns a defender host by ip, if any
func GetDefenderHost(ip string) (*DefenderEntry, error) {
if Config.defender == nil {
return nil, errors.New("defender is disabled")
}
return Config.defender.GetHost(ip)
}
// DeleteDefenderHost removes the specified IP address from the defender lists
func DeleteDefenderHost(ip string) bool {
if Config.defender == nil {
return false
}
return Config.defender.Unban(ip)
return Config.defender.DeleteHost(ip)
}
// GetDefenderScore returns the score for the given IP

View file

@ -1,6 +1,7 @@
package common
import (
"encoding/json"
"fmt"
"net"
"os"
@ -129,8 +130,11 @@ func TestDefenderIntegration(t *testing.T) {
assert.False(t, IsBanned(ip))
assert.Nil(t, GetDefenderBanTime(ip))
assert.False(t, Unban(ip))
assert.False(t, DeleteDefenderHost(ip))
assert.Equal(t, 0, GetDefenderScore(ip))
_, err := GetDefenderHost(ip)
assert.Error(t, err)
assert.Nil(t, GetDefenderHosts())
Config.DefenderConfig = DefenderConfig{
Enabled: true,
@ -143,7 +147,7 @@ func TestDefenderIntegration(t *testing.T) {
EntriesSoftLimit: 100,
EntriesHardLimit: 150,
}
err := Initialize(Config)
err = Initialize(Config)
assert.Error(t, err)
Config.DefenderConfig.Threshold = 3
err = Initialize(Config)
@ -153,16 +157,27 @@ func TestDefenderIntegration(t *testing.T) {
AddDefenderEvent(ip, HostEventNoLoginTried)
assert.False(t, IsBanned(ip))
assert.Equal(t, 2, GetDefenderScore(ip))
assert.False(t, Unban(ip))
entry, err := GetDefenderHost(ip)
assert.NoError(t, err)
asJSON, err := json.Marshal(&entry)
assert.NoError(t, err)
assert.Equal(t, `{"id":"3132372e312e312e31","ip":"127.1.1.1","score":2}`, string(asJSON), "entry %v", entry)
assert.True(t, DeleteDefenderHost(ip))
assert.Nil(t, GetDefenderBanTime(ip))
AddDefenderEvent(ip, HostEventLoginFailed)
AddDefenderEvent(ip, HostEventNoLoginTried)
assert.True(t, IsBanned(ip))
assert.Equal(t, 0, GetDefenderScore(ip))
assert.NotNil(t, GetDefenderBanTime(ip))
assert.True(t, Unban(ip))
assert.Len(t, GetDefenderHosts(), 1)
entry, err = GetDefenderHost(ip)
assert.NoError(t, err)
assert.False(t, entry.BanTime.IsZero())
assert.True(t, DeleteDefenderHost(ip))
assert.Len(t, GetDefenderHosts(), 0)
assert.Nil(t, GetDefenderBanTime(ip))
assert.False(t, Unban(ip))
assert.False(t, DeleteDefenderHost(ip))
Config = configCopy
}

View file

@ -1,6 +1,7 @@
package common
import (
"encoding/hex"
"encoding/json"
"fmt"
"net"
@ -11,6 +12,7 @@ import (
"github.com/yl2chen/cidranger"
"github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/utils"
)
@ -26,13 +28,50 @@ const (
HostEventLimitExceeded
)
// DefenderEntry defines a defender entry
type DefenderEntry struct {
IP string `json:"ip"`
Score int `json:"score,omitempty"`
BanTime time.Time `json:"ban_time,omitempty"`
}
// GetID returns an unique ID for a defender entry
func (d *DefenderEntry) GetID() string {
return hex.EncodeToString([]byte(d.IP))
}
// GetBanTime returns the ban time for a defender entry as string
func (d *DefenderEntry) GetBanTime() string {
if d.BanTime.IsZero() {
return ""
}
return d.BanTime.UTC().Format(time.RFC3339)
}
// MarshalJSON returns the JSON encoding of a DefenderEntry.
func (d *DefenderEntry) MarshalJSON() ([]byte, error) {
return json.Marshal(&struct {
ID string `json:"id"`
IP string `json:"ip"`
Score int `json:"score,omitempty"`
BanTime string `json:"ban_time,omitempty"`
}{
ID: d.GetID(),
IP: d.IP,
Score: d.Score,
BanTime: d.GetBanTime(),
})
}
// Defender defines the interface that a defender must implements
type Defender interface {
GetHosts() []*DefenderEntry
GetHost(ip string) (*DefenderEntry, error)
AddEvent(ip string, event HostEvent)
IsBanned(ip string) bool
GetBanTime(ip string) *time.Time
GetScore(ip string) int
Unban(ip string) bool
DeleteHost(ip string) bool
Reload() error
}
@ -190,6 +229,50 @@ func (d *memoryDefender) Reload() error {
return nil
}
// GetHosts returns hosts that are banned or for which some violations have been detected
func (d *memoryDefender) GetHosts() []*DefenderEntry {
d.RLock()
defer d.RUnlock()
var result []*DefenderEntry
for k, v := range d.banned {
result = append(result, &DefenderEntry{
IP: k,
BanTime: v,
})
}
for k, v := range d.hosts {
result = append(result, &DefenderEntry{
IP: k,
Score: v.TotalScore,
})
}
return result
}
// GetHost returns a defender host by ip, if any
func (d *memoryDefender) GetHost(ip string) (*DefenderEntry, error) {
d.RLock()
defer d.RUnlock()
if banTime, ok := d.banned[ip]; ok {
return &DefenderEntry{
IP: ip,
BanTime: banTime,
}, nil
}
if ev, ok := d.hosts[ip]; ok {
return &DefenderEntry{
IP: ip,
Score: ev.TotalScore,
}, nil
}
return nil, dataprovider.NewRecordNotFoundError("host not found")
}
// IsBanned returns true if the specified IP is banned
// and increase ban time if the IP is found.
// This method must be called as soon as the client connects
@ -227,8 +310,8 @@ func (d *memoryDefender) IsBanned(ip string) bool {
return false
}
// Unban removes the specified IP address from the banned ones
func (d *memoryDefender) Unban(ip string) bool {
// DeleteHost removes the specified IP from the defender lists
func (d *memoryDefender) DeleteHost(ip string) bool {
d.Lock()
defer d.Unlock()
@ -237,6 +320,11 @@ func (d *memoryDefender) Unban(ip string) bool {
return true
}
if _, ok := d.hosts[ip]; ok {
delete(d.hosts, ip)
return true
}
return false
}
@ -250,6 +338,11 @@ func (d *memoryDefender) AddEvent(ip string, event HostEvent) {
return
}
// ignore events for already banned hosts
if _, ok := d.banned[ip]; ok {
return
}
var score int
switch event {

View file

@ -2,6 +2,7 @@ package common
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"net"
@ -72,6 +73,9 @@ func TestBasicDefender(t *testing.T) {
assert.False(t, defender.IsBanned("invalid ip"))
assert.Equal(t, 0, defender.countBanned())
assert.Equal(t, 0, defender.countHosts())
assert.Len(t, defender.GetHosts(), 0)
_, err = defender.GetHost("10.8.0.4")
assert.Error(t, err)
defender.AddEvent("172.16.1.4", HostEventLoginFailed)
defender.AddEvent("192.168.8.4", HostEventUserNotFound)
@ -83,16 +87,39 @@ func TestBasicDefender(t *testing.T) {
assert.Equal(t, 1, defender.countHosts())
assert.Equal(t, 0, defender.countBanned())
assert.Equal(t, 1, defender.GetScore(testIP))
if assert.Len(t, defender.GetHosts(), 1) {
assert.Equal(t, 1, defender.GetHosts()[0].Score)
assert.True(t, defender.GetHosts()[0].BanTime.IsZero())
assert.Empty(t, defender.GetHosts()[0].GetBanTime())
}
host, err := defender.GetHost(testIP)
assert.NoError(t, err)
assert.Equal(t, 1, host.Score)
assert.Empty(t, host.GetBanTime())
assert.Nil(t, defender.GetBanTime(testIP))
defender.AddEvent(testIP, HostEventLimitExceeded)
assert.Equal(t, 1, defender.countHosts())
assert.Equal(t, 0, defender.countBanned())
assert.Equal(t, 4, defender.GetScore(testIP))
if assert.Len(t, defender.GetHosts(), 1) {
assert.Equal(t, 4, defender.GetHosts()[0].Score)
}
defender.AddEvent(testIP, HostEventNoLoginTried)
defender.AddEvent(testIP, HostEventNoLoginTried)
assert.Equal(t, 0, defender.countHosts())
assert.Equal(t, 1, defender.countBanned())
assert.Equal(t, 0, defender.GetScore(testIP))
assert.NotNil(t, defender.GetBanTime(testIP))
if assert.Len(t, defender.GetHosts(), 1) {
assert.Equal(t, 0, defender.GetHosts()[0].Score)
assert.False(t, defender.GetHosts()[0].BanTime.IsZero())
assert.NotEmpty(t, defender.GetHosts()[0].GetBanTime())
assert.Equal(t, hex.EncodeToString([]byte(testIP)), defender.GetHosts()[0].GetID())
}
host, err = defender.GetHost(testIP)
assert.NoError(t, err)
assert.Equal(t, 0, host.Score)
assert.NotEmpty(t, host.GetBanTime())
// now test cleanup, testIP is already banned
testIP1 := "12.34.56.79"
@ -143,8 +170,8 @@ func TestBasicDefender(t *testing.T) {
assert.True(t, newBanTime.After(*banTime))
}
assert.True(t, defender.Unban(testIP3))
assert.False(t, defender.Unban(testIP3))
assert.True(t, defender.DeleteHost(testIP3))
assert.False(t, defender.DeleteHost(testIP3))
err = os.Remove(slFile)
assert.NoError(t, err)

View file

@ -403,6 +403,13 @@ func (e *RecordNotFoundError) Error() string {
return fmt.Sprintf("not found: %s", e.err)
}
// NewRecordNotFoundError returns a not found error
func NewRecordNotFoundError(error string) *RecordNotFoundError {
return &RecordNotFoundError{
err: error,
}
}
// GetQuotaTracking returns the configured mode for user's quota tracking
func GetQuotaTracking() int {
return config.TrackQuota

View file

@ -1,6 +1,7 @@
package httpd
import (
"encoding/hex"
"errors"
"fmt"
"net"
@ -12,6 +13,38 @@ import (
"github.com/drakkan/sftpgo/common"
)
func getDefenderHosts(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, common.GetDefenderHosts())
}
func getDefenderHostByID(w http.ResponseWriter, r *http.Request) {
ip, err := getIPFromID(r)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
host, err := common.GetDefenderHost(ip)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
render.JSON(w, r, host)
}
func deleteDefenderHostByID(w http.ResponseWriter, r *http.Request) {
ip, err := getIPFromID(r)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
if !common.DeleteDefenderHost(ip) {
sendAPIResponse(w, r, nil, "Not found", http.StatusNotFound)
return
}
sendAPIResponse(w, r, nil, "OK", http.StatusOK)
}
func getBanTime(w http.ResponseWriter, r *http.Request) {
ip := r.URL.Query().Get("ip")
err := validateIPAddress(ip)
@ -64,13 +97,26 @@ func unban(w http.ResponseWriter, r *http.Request) {
return
}
if common.Unban(ip) {
if common.DeleteDefenderHost(ip) {
sendAPIResponse(w, r, nil, "OK", http.StatusOK)
} else {
sendAPIResponse(w, r, nil, "Not found", http.StatusNotFound)
}
}
func getIPFromID(r *http.Request) (string, error) {
decoded, err := hex.DecodeString(getURLParam(r, "id"))
if err != nil {
return "", errors.New("invalid host id")
}
ip := string(decoded)
err = validateIPAddress(ip)
if err != nil {
return "", err
}
return ip, nil
}
func validateIPAddress(ip string) error {
if ip == "" {
return errors.New("ip address is required")

View file

@ -282,7 +282,7 @@ func RestoreUsers(users []dataprovider.User, inputFile string, mode, scanQuota i
if scanQuota == 1 || (scanQuota == 2 && user.HasQuotaRestrictions()) {
if common.QuotaScans.AddUserQuotaScan(user.Username) {
logger.Debug(logSender, "", "starting quota scan for restored user: %#v", user.Username)
go doQuotaScan(user) //nolint:errcheck
go doUserQuotaScan(user) //nolint:errcheck
}
}
}

View file

@ -17,15 +17,31 @@ const (
quotaUpdateModeReset = "reset"
)
func getQuotaScans(w http.ResponseWriter, r *http.Request) {
type quotaUsage struct {
UsedQuotaSize int64 `json:"used_quota_size"`
UsedQuotaFiles int `json:"used_quota_files"`
}
func getUsersQuotaScans(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, common.QuotaScans.GetUsersQuotaScans())
}
func getVFolderQuotaScans(w http.ResponseWriter, r *http.Request) {
func getFoldersQuotaScans(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, common.QuotaScans.GetVFoldersQuotaScans())
}
func updateUserQuotaUsage(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
var usage quotaUsage
err := render.DecodeJSON(r.Body, &usage)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
doUpdateUserQuotaUsage(w, r, getURLParam(r, "username"), usage)
}
func updateUserQuotaUsageCompat(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
var u dataprovider.User
err := render.DecodeJSON(r.Body, &u)
@ -33,7 +49,74 @@ func updateUserQuotaUsage(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
if u.UsedQuotaFiles < 0 || u.UsedQuotaSize < 0 {
usage := quotaUsage{
UsedQuotaSize: u.UsedQuotaSize,
UsedQuotaFiles: u.UsedQuotaFiles,
}
doUpdateUserQuotaUsage(w, r, u.Username, usage)
}
func updateFolderQuotaUsage(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
var usage quotaUsage
err := render.DecodeJSON(r.Body, &usage)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
doUpdateFolderQuotaUsage(w, r, getURLParam(r, "name"), usage)
}
func updateFolderQuotaUsageCompat(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
var f vfs.BaseVirtualFolder
err := render.DecodeJSON(r.Body, &f)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
usage := quotaUsage{
UsedQuotaSize: f.UsedQuotaSize,
UsedQuotaFiles: f.UsedQuotaFiles,
}
doUpdateFolderQuotaUsage(w, r, f.Name, usage)
}
func startUserQuotaScan(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
doStartUserQuotaScan(w, r, getURLParam(r, "username"))
}
func startUserQuotaScanCompat(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
var u dataprovider.User
err := render.DecodeJSON(r.Body, &u)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
doStartUserQuotaScan(w, r, u.Username)
}
func startFolderQuotaScan(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
doStartFolderQuotaScan(w, r, getURLParam(r, "name"))
}
func startFolderQuotaScanCompat(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
var f vfs.BaseVirtualFolder
err := render.DecodeJSON(r.Body, &f)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
doStartFolderQuotaScan(w, r, f.Name)
}
func doUpdateUserQuotaUsage(w http.ResponseWriter, r *http.Request, username string, usage quotaUsage) {
if usage.UsedQuotaFiles < 0 || usage.UsedQuotaSize < 0 {
sendAPIResponse(w, r, errors.New("invalid used quota parameters, negative values are not allowed"),
"", http.StatusBadRequest)
return
@ -43,7 +126,7 @@ func updateUserQuotaUsage(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
user, err := dataprovider.UserExists(u.Username)
user, err := dataprovider.UserExists(username)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
@ -58,7 +141,7 @@ func updateUserQuotaUsage(w http.ResponseWriter, r *http.Request) {
return
}
defer common.QuotaScans.RemoveUserQuotaScan(user.Username)
err = dataprovider.UpdateUserQuota(&user, u.UsedQuotaFiles, u.UsedQuotaSize, mode == quotaUpdateModeReset)
err = dataprovider.UpdateUserQuota(&user, usage.UsedQuotaFiles, usage.UsedQuotaSize, mode == quotaUpdateModeReset)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
} else {
@ -66,15 +149,8 @@ func updateUserQuotaUsage(w http.ResponseWriter, r *http.Request) {
}
}
func updateVFolderQuotaUsage(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
var f vfs.BaseVirtualFolder
err := render.DecodeJSON(r.Body, &f)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
if f.UsedQuotaFiles < 0 || f.UsedQuotaSize < 0 {
func doUpdateFolderQuotaUsage(w http.ResponseWriter, r *http.Request, name string, usage quotaUsage) {
if usage.UsedQuotaFiles < 0 || usage.UsedQuotaSize < 0 {
sendAPIResponse(w, r, errors.New("invalid used quota parameters, negative values are not allowed"),
"", http.StatusBadRequest)
return
@ -84,7 +160,7 @@ func updateVFolderQuotaUsage(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
folder, err := dataprovider.GetFolderByName(f.Name)
folder, err := dataprovider.GetFolderByName(name)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
@ -94,7 +170,7 @@ func updateVFolderQuotaUsage(w http.ResponseWriter, r *http.Request) {
return
}
defer common.QuotaScans.RemoveVFolderQuotaScan(folder.Name)
err = dataprovider.UpdateVirtualFolderQuota(&folder, f.UsedQuotaFiles, f.UsedQuotaSize, mode == quotaUpdateModeReset)
err = dataprovider.UpdateVirtualFolderQuota(&folder, usage.UsedQuotaFiles, usage.UsedQuotaSize, mode == quotaUpdateModeReset)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
} else {
@ -102,57 +178,43 @@ func updateVFolderQuotaUsage(w http.ResponseWriter, r *http.Request) {
}
}
func startQuotaScan(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
func doStartUserQuotaScan(w http.ResponseWriter, r *http.Request, username string) {
if dataprovider.GetQuotaTracking() == 0 {
sendAPIResponse(w, r, nil, "Quota tracking is disabled!", http.StatusForbidden)
return
}
var u dataprovider.User
err := render.DecodeJSON(r.Body, &u)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
user, err := dataprovider.UserExists(u.Username)
user, err := dataprovider.UserExists(username)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
if common.QuotaScans.AddUserQuotaScan(user.Username) {
go doQuotaScan(user) //nolint:errcheck
sendAPIResponse(w, r, err, "Scan started", http.StatusAccepted)
} else {
if !common.QuotaScans.AddUserQuotaScan(user.Username) {
sendAPIResponse(w, r, err, "Another scan is already in progress", http.StatusConflict)
return
}
go doUserQuotaScan(user) //nolint:errcheck
sendAPIResponse(w, r, err, "Scan started", http.StatusAccepted)
}
func startVFolderQuotaScan(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
func doStartFolderQuotaScan(w http.ResponseWriter, r *http.Request, name string) {
if dataprovider.GetQuotaTracking() == 0 {
sendAPIResponse(w, r, nil, "Quota tracking is disabled!", http.StatusForbidden)
return
}
var f vfs.BaseVirtualFolder
err := render.DecodeJSON(r.Body, &f)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
folder, err := dataprovider.GetFolderByName(f.Name)
folder, err := dataprovider.GetFolderByName(name)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
if common.QuotaScans.AddVFolderQuotaScan(folder.Name) {
go doFolderQuotaScan(folder) //nolint:errcheck
sendAPIResponse(w, r, err, "Scan started", http.StatusAccepted)
} else {
if !common.QuotaScans.AddVFolderQuotaScan(folder.Name) {
sendAPIResponse(w, r, err, "Another scan is already in progress", http.StatusConflict)
return
}
go doFolderQuotaScan(folder) //nolint:errcheck
sendAPIResponse(w, r, err, "Scan started", http.StatusAccepted)
}
func doQuotaScan(user dataprovider.User) error {
func doUserQuotaScan(user dataprovider.User) error {
defer common.QuotaScans.RemoveUserQuotaScan(user.Username)
numFiles, size, err := user.ScanQuota()
if err != nil {

View file

@ -35,6 +35,7 @@ const (
userTokenPath = "/api/v2/user/token"
userLogoutPath = "/api/v2/user/logout"
activeConnectionsPath = "/api/v2/connections"
quotasBasePath = "/api/v2/quotas"
quotaScanPath = "/api/v2/quota-scans"
quotaScanVFolderPath = "/api/v2/folder-quota-scans"
userPath = "/api/v2/users"
@ -45,6 +46,7 @@ const (
loadDataPath = "/api/v2/loaddata"
updateUsedQuotaPath = "/api/v2/quota-update"
updateFolderUsedQuotaPath = "/api/v2/folder-quota-update"
defenderHosts = "/api/v2/defender/hosts"
defenderBanTime = "/api/v2/defender/bantime"
defenderUnban = "/api/v2/defender/unban"
defenderScore = "/api/v2/defender/score"
@ -75,8 +77,8 @@ const (
webMaintenancePathDefault = "/web/admin/maintenance"
webBackupPathDefault = "/web/admin/backup"
webRestorePathDefault = "/web/admin/restore"
webScanVFolderPathDefault = "/web/admin/folder-quota-scans"
webQuotaScanPathDefault = "/web/admin/quota-scans"
webScanVFolderPathDefault = "/web/admin/quotas/scanfolder"
webQuotaScanPathDefault = "/web/admin/quotas/scanuser"
webChangeAdminPwdPathDefault = "/web/admin/changepwd"
webTemplateUserDefault = "/web/admin/template/user"
webTemplateFolderDefault = "/web/admin/template/folder"

View file

@ -46,68 +46,72 @@ import (
)
const (
defaultUsername = "test_user"
defaultPassword = "test_password"
testPubKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0= nicola@p1"
testPubKey1 = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCd60+/j+y8f0tLftihWV1YN9RSahMI9btQMDIMqts/jeNbD8jgoogM3nhF7KxfcaMKURuD47KC4Ey6iAJUJ0sWkSNNxOcIYuvA+5MlspfZDsa8Ag76Fe1vyz72WeHMHMeh/hwFo2TeIeIXg480T1VI6mzfDrVp2GzUx0SS0dMsQBjftXkuVR8YOiOwMCAH2a//M1OrvV7d/NBk6kBN0WnuIBb2jKm15PAA7+jQQG7tzwk2HedNH3jeL5GH31xkSRwlBczRK0xsCQXehAlx6cT/e/s44iJcJTHfpPKoSk6UAhPJYe7Z1QnuoawY9P9jQaxpyeImBZxxUEowhjpj2avBxKdRGBVK8R7EL8tSOeLbhdyWe5Mwc1+foEbq9Zz5j5Kd+hn3Wm1UnsGCrXUUUoZp1jnlNl0NakCto+5KmqnT9cHxaY+ix2RLUWAZyVFlRq71OYux1UHJnEJPiEI1/tr4jFBSL46qhQZv/TfpkfVW8FLz0lErfqu0gQEZnNHr3Fc= nicola@p1"
defaultTokenAuthUser = "admin"
defaultTokenAuthPass = "password"
altAdminUsername = "newTestAdmin"
altAdminPassword = "password1"
csrfFormToken = "_form_token"
tokenPath = "/api/v2/token"
userTokenPath = "/api/v2/user/token"
userLogoutPath = "/api/v2/user/logout"
userPath = "/api/v2/users"
adminPath = "/api/v2/admins"
adminPwdPath = "/api/v2/admin/changepwd"
folderPath = "/api/v2/folders"
activeConnectionsPath = "/api/v2/connections"
serverStatusPath = "/api/v2/status"
quotaScanPath = "/api/v2/quota-scans"
quotaScanVFolderPath = "/api/v2/folder-quota-scans"
updateUsedQuotaPath = "/api/v2/quota-update"
updateFolderUsedQuotaPath = "/api/v2/folder-quota-update"
defenderUnban = "/api/v2/defender/unban"
versionPath = "/api/v2/version"
logoutPath = "/api/v2/logout"
userPwdPath = "/api/v2/user/changepwd"
userPublicKeysPath = "/api/v2/user/publickeys"
userReadFolderPath = "/api/v2/user/folder"
userGetFilePath = "/api/v2/user/file"
userStreamZipPath = "/api/v2/user/streamzip"
healthzPath = "/healthz"
webBasePath = "/web"
webBasePathAdmin = "/web/admin"
webAdminSetupPath = "/web/admin/setup"
webLoginPath = "/web/admin/login"
webLogoutPath = "/web/admin/logout"
webUsersPath = "/web/admin/users"
webUserPath = "/web/admin/user"
webFoldersPath = "/web/admin/folders"
webFolderPath = "/web/admin/folder"
webConnectionsPath = "/web/admin/connections"
webStatusPath = "/web/admin/status"
webAdminsPath = "/web/admin/managers"
webAdminPath = "/web/admin/manager"
webMaintenancePath = "/web/admin/maintenance"
webRestorePath = "/web/admin/restore"
webChangeAdminPwdPath = "/web/admin/changepwd"
webTemplateUser = "/web/admin/template/user"
webTemplateFolder = "/web/admin/template/folder"
webBasePathClient = "/web/client"
webClientLoginPath = "/web/client/login"
webClientFilesPath = "/web/client/files"
webClientDirContentsPath = "/web/client/listdir"
webClientDownloadZipPath = "/web/client/downloadzip"
webClientCredentialsPath = "/web/client/credentials"
webChangeClientPwdPath = "/web/client/changepwd"
webChangeClientKeysPath = "/web/client/managekeys"
webClientLogoutPath = "/web/client/logout"
httpBaseURL = "http://127.0.0.1:8081"
sftpServerAddr = "127.0.0.1:8022"
configDir = ".."
httpsCert = `-----BEGIN CERTIFICATE-----
defaultUsername = "test_user"
defaultPassword = "test_password"
testPubKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0= nicola@p1"
testPubKey1 = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCd60+/j+y8f0tLftihWV1YN9RSahMI9btQMDIMqts/jeNbD8jgoogM3nhF7KxfcaMKURuD47KC4Ey6iAJUJ0sWkSNNxOcIYuvA+5MlspfZDsa8Ag76Fe1vyz72WeHMHMeh/hwFo2TeIeIXg480T1VI6mzfDrVp2GzUx0SS0dMsQBjftXkuVR8YOiOwMCAH2a//M1OrvV7d/NBk6kBN0WnuIBb2jKm15PAA7+jQQG7tzwk2HedNH3jeL5GH31xkSRwlBczRK0xsCQXehAlx6cT/e/s44iJcJTHfpPKoSk6UAhPJYe7Z1QnuoawY9P9jQaxpyeImBZxxUEowhjpj2avBxKdRGBVK8R7EL8tSOeLbhdyWe5Mwc1+foEbq9Zz5j5Kd+hn3Wm1UnsGCrXUUUoZp1jnlNl0NakCto+5KmqnT9cHxaY+ix2RLUWAZyVFlRq71OYux1UHJnEJPiEI1/tr4jFBSL46qhQZv/TfpkfVW8FLz0lErfqu0gQEZnNHr3Fc= nicola@p1"
defaultTokenAuthUser = "admin"
defaultTokenAuthPass = "password"
altAdminUsername = "newTestAdmin"
altAdminPassword = "password1"
csrfFormToken = "_form_token"
tokenPath = "/api/v2/token"
userTokenPath = "/api/v2/user/token"
userLogoutPath = "/api/v2/user/logout"
userPath = "/api/v2/users"
adminPath = "/api/v2/admins"
adminPwdPath = "/api/v2/admin/changepwd"
folderPath = "/api/v2/folders"
activeConnectionsPath = "/api/v2/connections"
serverStatusPath = "/api/v2/status"
quotasBasePath = "/api/v2/quotas"
quotaScanPath = "/api/v2/quotas/users/scans"
quotaScanVFolderPath = "/api/v2/quotas/folders/scans"
quotaScanCompatPath = "/api/v2/quota-scans"
quotaScanVFolderCompatPath = "/api/v2/folder-quota-scans"
updateUsedQuotaCompatPath = "/api/v2/quota-update"
updateFolderUsedQuotaCompatPath = "/api/v2/folder-quota-update"
defenderHosts = "/api/v2/defender/hosts"
defenderUnban = "/api/v2/defender/unban"
versionPath = "/api/v2/version"
logoutPath = "/api/v2/logout"
userPwdPath = "/api/v2/user/changepwd"
userPublicKeysPath = "/api/v2/user/publickeys"
userReadFolderPath = "/api/v2/user/folder"
userGetFilePath = "/api/v2/user/file"
userStreamZipPath = "/api/v2/user/streamzip"
healthzPath = "/healthz"
webBasePath = "/web"
webBasePathAdmin = "/web/admin"
webAdminSetupPath = "/web/admin/setup"
webLoginPath = "/web/admin/login"
webLogoutPath = "/web/admin/logout"
webUsersPath = "/web/admin/users"
webUserPath = "/web/admin/user"
webFoldersPath = "/web/admin/folders"
webFolderPath = "/web/admin/folder"
webConnectionsPath = "/web/admin/connections"
webStatusPath = "/web/admin/status"
webAdminsPath = "/web/admin/managers"
webAdminPath = "/web/admin/manager"
webMaintenancePath = "/web/admin/maintenance"
webRestorePath = "/web/admin/restore"
webChangeAdminPwdPath = "/web/admin/changepwd"
webTemplateUser = "/web/admin/template/user"
webTemplateFolder = "/web/admin/template/folder"
webBasePathClient = "/web/client"
webClientLoginPath = "/web/client/login"
webClientFilesPath = "/web/client/files"
webClientDirContentsPath = "/web/client/listdir"
webClientDownloadZipPath = "/web/client/downloadzip"
webClientCredentialsPath = "/web/client/credentials"
webChangeClientPwdPath = "/web/client/changepwd"
webChangeClientKeysPath = "/web/client/managekeys"
webClientLogoutPath = "/web/client/logout"
httpBaseURL = "http://127.0.0.1:8081"
sftpServerAddr = "127.0.0.1:8022"
configDir = ".."
httpsCert = `-----BEGIN CERTIFICATE-----
MIICHTCCAaKgAwIBAgIUHnqw7QnB1Bj9oUsNpdb+ZkFPOxMwCgYIKoZIzj0EAwIw
RTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu
dGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDAyMDQwOTUzMDRaFw0zMDAyMDEw
@ -3144,6 +3148,10 @@ func TestDefenderAPI(t *testing.T) {
require.True(t, ok)
assert.Nil(t, banTime)
hosts, _, err := httpdtest.GetDefenderHosts(http.StatusOK)
require.NoError(t, err)
assert.Len(t, hosts, 0)
response, _, err = httpdtest.GetScore(ip, http.StatusOK)
require.NoError(t, err)
score, ok := response["score"]
@ -3153,6 +3161,9 @@ func TestDefenderAPI(t *testing.T) {
err = httpdtest.UnbanIP(ip, http.StatusNotFound)
require.NoError(t, err)
_, err = httpdtest.RemoveDefenderHostByIP(ip, http.StatusNotFound)
require.NoError(t, err)
common.AddDefenderEvent(ip, common.HostEventNoLoginTried)
response, _, err = httpdtest.GetScore(ip, http.StatusOK)
require.NoError(t, err)
@ -3160,12 +3171,37 @@ func TestDefenderAPI(t *testing.T) {
require.True(t, ok)
assert.Equal(t, float64(2), score)
hosts, _, err = httpdtest.GetDefenderHosts(http.StatusOK)
require.NoError(t, err)
if assert.Len(t, hosts, 1) {
host := hosts[0]
assert.Empty(t, host.GetBanTime())
assert.Equal(t, 2, host.Score)
assert.Equal(t, ip, host.IP)
}
host, _, err := httpdtest.GetDefenderHostByIP(ip, http.StatusOK)
assert.NoError(t, err)
assert.Empty(t, host.GetBanTime())
assert.Equal(t, 2, host.Score)
common.AddDefenderEvent(ip, common.HostEventNoLoginTried)
response, _, err = httpdtest.GetBanTime(ip, http.StatusOK)
require.NoError(t, err)
banTime, ok = response["date_time"]
require.True(t, ok)
assert.NotNil(t, banTime)
hosts, _, err = httpdtest.GetDefenderHosts(http.StatusOK)
require.NoError(t, err)
if assert.Len(t, hosts, 1) {
host := hosts[0]
assert.NotEmpty(t, host.GetBanTime())
assert.Equal(t, 0, host.Score)
assert.Equal(t, ip, host.IP)
}
host, _, err = httpdtest.GetDefenderHostByIP(ip, http.StatusOK)
assert.NoError(t, err)
assert.NotEmpty(t, host.GetBanTime())
assert.Equal(t, 0, host.Score)
err = httpdtest.UnbanIP(ip, http.StatusOK)
require.NoError(t, err)
@ -3173,6 +3209,28 @@ func TestDefenderAPI(t *testing.T) {
err = httpdtest.UnbanIP(ip, http.StatusNotFound)
require.NoError(t, err)
host, _, err = httpdtest.GetDefenderHostByIP(ip, http.StatusNotFound)
assert.NoError(t, err)
common.AddDefenderEvent(ip, common.HostEventNoLoginTried)
common.AddDefenderEvent(ip, common.HostEventNoLoginTried)
hosts, _, err = httpdtest.GetDefenderHosts(http.StatusOK)
require.NoError(t, err)
assert.Len(t, hosts, 1)
_, err = httpdtest.RemoveDefenderHostByIP(ip, http.StatusOK)
assert.NoError(t, err)
host, _, err = httpdtest.GetDefenderHostByIP(ip, http.StatusNotFound)
assert.NoError(t, err)
_, err = httpdtest.RemoveDefenderHostByIP(ip, http.StatusNotFound)
assert.NoError(t, err)
host, _, err = httpdtest.GetDefenderHostByIP("invalid_ip", http.StatusBadRequest)
assert.NoError(t, err)
_, err = httpdtest.RemoveDefenderHostByIP("invalid_ip", http.StatusBadRequest)
assert.NoError(t, err)
err = common.Initialize(oldConfig)
require.NoError(t, err)
}
@ -3899,7 +3957,11 @@ func TestUpdateUserQuotaUsageMock(t *testing.T) {
checkResponseCode(t, http.StatusCreated, rr)
err = render.DecodeJSON(rr.Body, &user)
assert.NoError(t, err)
req, _ = http.NewRequest(http.MethodPut, updateUsedQuotaPath, bytes.NewBuffer(userAsJSON))
req, _ = http.NewRequest(http.MethodPut, path.Join(quotasBasePath, "users", u.Username, "usage"), bytes.NewBuffer(userAsJSON))
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
req, _ = http.NewRequest(http.MethodPut, updateUsedQuotaCompatPath, bytes.NewBuffer(userAsJSON))
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
@ -3914,7 +3976,7 @@ func TestUpdateUserQuotaUsageMock(t *testing.T) {
// now update only quota size
u.UsedQuotaFiles = 0
userAsJSON = getUserAsJSON(t, u)
req, _ = http.NewRequest(http.MethodPut, updateUsedQuotaPath+"?mode=add", bytes.NewBuffer(userAsJSON))
req, _ = http.NewRequest(http.MethodPut, path.Join(quotasBasePath, "users", u.Username, "usage")+"?mode=add", bytes.NewBuffer(userAsJSON))
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
@ -3930,7 +3992,7 @@ func TestUpdateUserQuotaUsageMock(t *testing.T) {
u.UsedQuotaFiles = usedQuotaFiles
u.UsedQuotaSize = 0
userAsJSON = getUserAsJSON(t, u)
req, _ = http.NewRequest(http.MethodPut, updateUsedQuotaPath+"?mode=add", bytes.NewBuffer(userAsJSON))
req, _ = http.NewRequest(http.MethodPut, path.Join(quotasBasePath, "users", u.Username, "usage")+"?mode=add", bytes.NewBuffer(userAsJSON))
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
@ -3942,12 +4004,16 @@ func TestUpdateUserQuotaUsageMock(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, usedQuotaFiles*2, user.UsedQuotaFiles)
assert.Equal(t, usedQuotaSize*2, user.UsedQuotaSize)
req, _ = http.NewRequest(http.MethodPut, updateUsedQuotaPath, bytes.NewBuffer([]byte("string")))
req, _ = http.NewRequest(http.MethodPut, updateUsedQuotaCompatPath, bytes.NewBuffer([]byte("string")))
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr)
req, _ = http.NewRequest(http.MethodPut, path.Join(quotasBasePath, "users", u.Username, "usage"), bytes.NewBuffer([]byte("string")))
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr)
assert.True(t, common.QuotaScans.AddUserQuotaScan(user.Username))
req, _ = http.NewRequest(http.MethodPut, updateUsedQuotaPath, bytes.NewBuffer(userAsJSON))
req, _ = http.NewRequest(http.MethodPut, path.Join(quotasBasePath, "users", u.Username, "usage"), bytes.NewBuffer(userAsJSON))
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusConflict, rr)
@ -4198,7 +4264,7 @@ func TestDeleteUserInvalidParamsMock(t *testing.T) {
func TestGetQuotaScansMock(t *testing.T) {
token, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
assert.NoError(t, err)
req, err := http.NewRequest("GET", quotaScanPath, nil)
req, err := http.NewRequest(http.MethodGet, quotaScanPath, nil)
assert.NoError(t, err)
setBearerForReq(req, token)
rr := executeRequest(req)
@ -4222,61 +4288,47 @@ func TestStartQuotaScanMock(t *testing.T) {
assert.NoError(t, err)
}
// simulate a duplicate quota scan
userAsJSON = getUserAsJSON(t, user)
common.QuotaScans.AddUserQuotaScan(user.Username)
req, _ = http.NewRequest(http.MethodPost, quotaScanPath, bytes.NewBuffer(userAsJSON))
req, _ = http.NewRequest(http.MethodPost, path.Join(quotasBasePath, "users", user.Username, "scan"), nil)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusConflict, rr)
assert.True(t, common.QuotaScans.RemoveUserQuotaScan(user.Username))
userAsJSON = getUserAsJSON(t, user)
req, _ = http.NewRequest(http.MethodPost, quotaScanPath, bytes.NewBuffer(userAsJSON))
req, _ = http.NewRequest(http.MethodPost, path.Join(quotasBasePath, "users", user.Username, "scan"), nil)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusAccepted, rr)
for {
var scans []common.ActiveQuotaScan
req, _ = http.NewRequest(http.MethodGet, quotaScanPath, nil)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
err = render.DecodeJSON(rr.Body, &scans)
if !assert.NoError(t, err, "Error getting active scans") {
break
}
if len(scans) == 0 {
break
}
time.Sleep(100 * time.Millisecond)
}
waitForUsersQuotaScan(t, token)
_, err = os.Stat(user.HomeDir)
if err != nil && os.IsNotExist(err) {
err = os.MkdirAll(user.HomeDir, os.ModePerm)
assert.NoError(t, err)
}
req, _ = http.NewRequest(http.MethodPost, quotaScanPath, bytes.NewBuffer(userAsJSON))
req, _ = http.NewRequest(http.MethodPost, path.Join(quotasBasePath, "users", user.Username, "scan"), nil)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusAccepted, rr)
for {
var scans []common.ActiveQuotaScan
req, _ = http.NewRequest(http.MethodGet, quotaScanPath, nil)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
err = render.DecodeJSON(rr.Body, &scans)
if !assert.NoError(t, err) {
assert.Fail(t, err.Error(), "Error getting active scans")
break
}
if len(scans) == 0 {
break
}
time.Sleep(100 * time.Millisecond)
}
waitForUsersQuotaScan(t, token)
req, _ = http.NewRequest(http.MethodPost, path.Join(quotasBasePath, "users", user.Username, "scan"), nil)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusAccepted, rr)
waitForUsersQuotaScan(t, token)
asJSON, err := json.Marshal(user)
assert.NoError(t, err)
req, _ = http.NewRequest(http.MethodPost, quotaScanCompatPath, bytes.NewBuffer(asJSON))
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusAccepted, rr)
waitForUsersQuotaScan(t, token)
req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil)
setBearerForReq(req, token)
@ -4308,7 +4360,11 @@ func TestUpdateFolderQuotaUsageMock(t *testing.T) {
checkResponseCode(t, http.StatusCreated, rr)
err = render.DecodeJSON(rr.Body, &folder)
assert.NoError(t, err)
req, _ = http.NewRequest(http.MethodPut, updateFolderUsedQuotaPath, bytes.NewBuffer(folderAsJSON))
req, _ = http.NewRequest(http.MethodPut, path.Join(quotasBasePath, "folders", folder.Name, "usage"), bytes.NewBuffer(folderAsJSON))
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
req, _ = http.NewRequest(http.MethodPut, updateFolderUsedQuotaCompatPath, bytes.NewBuffer(folderAsJSON))
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
@ -4325,7 +4381,8 @@ func TestUpdateFolderQuotaUsageMock(t *testing.T) {
f.UsedQuotaFiles = 0
folderAsJSON, err = json.Marshal(f)
assert.NoError(t, err)
req, _ = http.NewRequest(http.MethodPut, updateFolderUsedQuotaPath+"?mode=add", bytes.NewBuffer(folderAsJSON))
req, _ = http.NewRequest(http.MethodPut, path.Join(quotasBasePath, "folders", folder.Name, "usage")+"?mode=add",
bytes.NewBuffer(folderAsJSON))
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
@ -4343,7 +4400,8 @@ func TestUpdateFolderQuotaUsageMock(t *testing.T) {
f.UsedQuotaFiles = 1
folderAsJSON, err = json.Marshal(f)
assert.NoError(t, err)
req, _ = http.NewRequest(http.MethodPut, updateFolderUsedQuotaPath+"?mode=add", bytes.NewBuffer(folderAsJSON))
req, _ = http.NewRequest(http.MethodPut, path.Join(quotasBasePath, "folders", folder.Name, "usage")+"?mode=add",
bytes.NewBuffer(folderAsJSON))
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
@ -4356,13 +4414,19 @@ func TestUpdateFolderQuotaUsageMock(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, usedQuotaFiles*2, folderGet.UsedQuotaFiles)
assert.Equal(t, usedQuotaSize*2, folderGet.UsedQuotaSize)
req, _ = http.NewRequest(http.MethodPut, updateFolderUsedQuotaPath, bytes.NewBuffer([]byte("string")))
req, _ = http.NewRequest(http.MethodPut, updateFolderUsedQuotaCompatPath, bytes.NewBuffer([]byte("string")))
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr)
req, _ = http.NewRequest(http.MethodPut, path.Join(quotasBasePath, "folders", folder.Name, "usage"),
bytes.NewBuffer([]byte("not a json")))
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr)
assert.True(t, common.QuotaScans.AddVFolderQuotaScan(folderName))
req, _ = http.NewRequest(http.MethodPut, updateFolderUsedQuotaPath, bytes.NewBuffer(folderAsJSON))
req, _ = http.NewRequest(http.MethodPut, path.Join(quotasBasePath, "folders", folder.Name, "usage"),
bytes.NewBuffer(folderAsJSON))
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusConflict, rr)
@ -4396,7 +4460,7 @@ func TestStartFolderQuotaScanMock(t *testing.T) {
}
// simulate a duplicate quota scan
common.QuotaScans.AddVFolderQuotaScan(folderName)
req, _ = http.NewRequest(http.MethodPost, quotaScanVFolderPath, bytes.NewBuffer(folderAsJSON))
req, _ = http.NewRequest(http.MethodPost, path.Join(quotasBasePath, "folders", folder.Name, "scan"), nil)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusConflict, rr)
@ -4407,25 +4471,20 @@ func TestStartFolderQuotaScanMock(t *testing.T) {
err = os.MkdirAll(mappedPath, os.ModePerm)
assert.NoError(t, err)
}
req, _ = http.NewRequest(http.MethodPost, quotaScanVFolderPath, bytes.NewBuffer(folderAsJSON))
req, _ = http.NewRequest(http.MethodPost, path.Join(quotasBasePath, "folders", folder.Name, "scan"), nil)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusAccepted, rr)
var scans []common.ActiveVirtualFolderQuotaScan
for {
req, _ = http.NewRequest(http.MethodGet, quotaScanVFolderPath, nil)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
err = render.DecodeJSON(rr.Body, &scans)
if !assert.NoError(t, err, "Error getting active folders scans") {
break
}
if len(scans) == 0 {
break
}
time.Sleep(100 * time.Millisecond)
}
waitForFoldersQuotaScanPath(t, token)
asJSON, err := json.Marshal(folder)
assert.NoError(t, err)
req, _ = http.NewRequest(http.MethodPost, quotaScanVFolderCompatPath, bytes.NewBuffer(asJSON))
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusAccepted, rr)
waitForFoldersQuotaScanPath(t, token)
// cleanup
req, _ = http.NewRequest(http.MethodDelete, path.Join(folderPath, folderName), nil)
@ -4442,8 +4501,8 @@ func TestStartQuotaScanNonExistentUserMock(t *testing.T) {
token, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
assert.NoError(t, err)
user := getTestUser()
userAsJSON := getUserAsJSON(t, user)
req, _ := http.NewRequest(http.MethodPost, quotaScanPath, bytes.NewBuffer(userAsJSON))
req, _ := http.NewRequest(http.MethodPost, path.Join(quotasBasePath, "users", user.Username, "scan"), nil)
setBearerForReq(req, token)
rr := executeRequest(req)
checkResponseCode(t, http.StatusNotFound, rr)
@ -4452,7 +4511,7 @@ func TestStartQuotaScanNonExistentUserMock(t *testing.T) {
func TestStartQuotaScanBadUserMock(t *testing.T) {
token, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
assert.NoError(t, err)
req, _ := http.NewRequest(http.MethodPost, quotaScanPath, bytes.NewBuffer([]byte("invalid json")))
req, _ := http.NewRequest(http.MethodPost, quotaScanCompatPath, bytes.NewBuffer([]byte("invalid json")))
setBearerForReq(req, token)
rr := executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr)
@ -4461,7 +4520,7 @@ func TestStartQuotaScanBadUserMock(t *testing.T) {
func TestStartQuotaScanBadFolderMock(t *testing.T) {
token, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
assert.NoError(t, err)
req, _ := http.NewRequest(http.MethodPost, quotaScanVFolderPath, bytes.NewBuffer([]byte("invalid json")))
req, _ := http.NewRequest(http.MethodPost, quotaScanVFolderCompatPath, bytes.NewBuffer([]byte("invalid json")))
setBearerForReq(req, token)
rr := executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr)
@ -4474,9 +4533,7 @@ func TestStartQuotaScanNonExistentFolderMock(t *testing.T) {
MappedPath: os.TempDir(),
Name: "afolder",
}
folderAsJSON, err := json.Marshal(folder)
assert.NoError(t, err)
req, _ := http.NewRequest(http.MethodPost, quotaScanVFolderPath, bytes.NewBuffer(folderAsJSON))
req, _ := http.NewRequest(http.MethodPost, path.Join(quotasBasePath, "folders", folder.Name, "scan"), nil)
setBearerForReq(req, token)
rr := executeRequest(req)
checkResponseCode(t, http.StatusNotFound, rr)
@ -4667,6 +4724,16 @@ func TestLogout(t *testing.T) {
assert.Contains(t, rr.Body.String(), "Your token is no longer valid")
}
func TestDefenderAPIInvalidIDMock(t *testing.T) {
token, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
assert.NoError(t, err)
req, _ := http.NewRequest(http.MethodGet, path.Join(defenderHosts, "abc"), nil) // not hex id
setBearerForReq(req, token)
rr := executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "invalid host id")
}
func TestTokenHeaderCookie(t *testing.T) {
apiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
assert.NoError(t, err)
@ -8473,6 +8540,43 @@ func TestStaticFilesMock(t *testing.T) {
checkResponseCode(t, http.StatusOK, rr)
}
func waitForUsersQuotaScan(t *testing.T, token string) {
for {
var scans []common.ActiveQuotaScan
req, _ := http.NewRequest(http.MethodGet, quotaScanPath, nil)
setBearerForReq(req, token)
rr := executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
err := render.DecodeJSON(rr.Body, &scans)
if !assert.NoError(t, err, "Error getting active scans") {
break
}
if len(scans) == 0 {
break
}
time.Sleep(100 * time.Millisecond)
}
}
func waitForFoldersQuotaScanPath(t *testing.T, token string) {
var scans []common.ActiveVirtualFolderQuotaScan
for {
req, _ := http.NewRequest(http.MethodGet, quotaScanVFolderPath, nil)
setBearerForReq(req, token)
rr := executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
err := render.DecodeJSON(rr.Body, &scans)
if !assert.NoError(t, err, "Error getting active folders scans") {
break
}
if len(scans) == 0 {
break
}
time.Sleep(100 * time.Millisecond)
}
}
func waitTCPListening(address string) {
for {
conn, err := net.Dial("tcp", address)

View file

@ -869,7 +869,7 @@ func TestQuotaScanInvalidFs(t *testing.T) {
},
}
common.QuotaScans.AddUserQuotaScan(user.Username)
err := doQuotaScan(user)
err := doUserQuotaScan(user)
assert.Error(t, err)
}

View file

@ -17,7 +17,7 @@ info:
Several storage backends are supported and they are configurable per user, so you can serve a local directory for a user and an S3 bucket (or part of it) for another one.
SFTPGo also supports virtual folders, a virtual folder can use any of the supported storage backends. So you can have, for example, an S3 user that exposes a GCS bucket (or part of it) on a specified path and an encrypted local filesystem on another one.
Virtual folders can be private or shared among multiple users, for shared virtual folders you can define different quota limits for each user.
version: 2.0.9
version: 2.1.0
contact:
name: API support
url: 'https://github.com/drakkan/sftpgo'
@ -166,8 +166,8 @@ paths:
tags:
- admins
summary: Change admin password
description: Changes the password for the logged in admin. Please use /admin/changepwd instead
operationId: change_admin_pwd
description: Changes the password for the logged in admin. Please use '/admin/changepwd' instead
operationId: change_admin_password_deprecated
deprecated: true
requestBody:
required: true
@ -275,12 +275,93 @@ paths:
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/defender/bantime:
/defender/hosts:
get:
tags:
- defender
summary: Get hosts
description: Returns hosts that are banned or for which some violations have been detected
operationId: get_defender_hosts
responses:
'200':
description: successful operation
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/DefenderEntry'
'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'
/defender/hosts/{id}:
parameters:
- name: id
in: path
description: host id
required: true
schema:
type: string
get:
tags:
- defender
summary: Get host by id
description: Returns the host with the given id, if it exists
operationId: get_defender_host_by_id
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/DefenderEntry'
'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:
tags:
- defender
summary: Removes a host from the defender lists
description: Unbans the specified host or clears its violations
operationId: delete_defender_host_by_id
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
'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'
/defender/bantime:
get:
deprecated: true
tags:
- defender
summary: Get ban time
description: Returns the ban time for the specified IPv4/IPv6 address
description: Deprecated, please use '/defender/hosts', '/defender/hosts/{id}' instead
operationId: get_ban_time
parameters:
- in: query
@ -308,10 +389,11 @@ paths:
$ref: '#/components/responses/DefaultResponse'
/defender/unban:
post:
deprecated: true
tags:
- defender
summary: Unban
description: Removes the specified IPv4/IPv6 from the banned ones
description: Deprecated, please use '/defender/hosts/{id}' instead
operationId: unban_host
requestBody:
required: true
@ -344,10 +426,11 @@ paths:
$ref: '#/components/responses/DefaultResponse'
/defender/score:
get:
deprecated: true
tags:
- defender
summary: Get score
description: Returns the score for the specified IPv4/IPv6 address
description: Deprecated, please use '/defender/hosts', '/defender/hosts/{id}' instead
operationId: get_score
parameters:
- in: query
@ -373,13 +456,282 @@ paths:
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/quota-scans:
/quotas/users/scans:
get:
tags:
- quota
summary: Get active user quota scans
description: Returns the active user quota scans
operationId: get_users_quota_scans
responses:
'200':
description: successful operation
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/QuotaScan'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'500':
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/quotas/users/{username}/scan:
parameters:
- name: username
in: path
description: the username
required: true
schema:
type: string
post:
tags:
- quota
summary: Start a user quota scan
description: Starts a new quota scan for the given user. A quota scan updates the number of files and their total size for the specified user and the virtual folders, if any, included in his quota
operationId: start_user_quota_scan
responses:
'202':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
message: Scan started
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'500':
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/quotas/users/{username}/usage:
parameters:
- name: username
in: path
description: the username
required: true
schema:
type: string
- in: query
name: mode
required: false
description: the update mode specifies if the given quota usage values should be added or replace the current ones
schema:
type: string
enum:
- add
- reset
description: |
Update type:
* `add` - add the specified quota limits to the current used ones
* `reset` - reset the values to the specified ones. This is the default
example: reset
put:
tags:
- quota
summary: Update quota usage limits
description: Sets the current used quota limits for the given user
operationId: user_quota_update_usage
parameters:
- in: query
name: mode
required: false
description: the update mode specifies if the given quota usage values should be added or replace the current ones
schema:
type: string
enum:
- add
- reset
description: |
Update type:
* `add` - add the specified quota limits to the current used ones
* `reset` - reset the values to the specified ones. This is the default
example: reset
requestBody:
required: true
description: 'If used_quota_size and used_quota_files are missing they will default to 0, this means that if mode is "add" the current value, for the missing field, will remain unchanged, if mode is "reset" the missing field is set to 0'
content:
application/json:
schema:
$ref: '#/components/schemas/QuotaUsage'
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
message: Quota updated
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'500':
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/quotas/folders/scans:
get:
tags:
- quota
summary: Get active folder quota scans
description: Returns the active folder quota scans
operationId: get_folders_quota_scans
responses:
'200':
description: successful operation
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/FolderQuotaScan'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'500':
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/quotas/folders/{name}/scan:
parameters:
- name: name
in: path
description: folder name
required: true
schema:
type: string
post:
tags:
- quota
summary: Start a folder quota scan
description: Starts a new quota scan for the given folder. A quota scan update the number of files and their total size for the specified folder
operationId: start_folder_quota_scan
responses:
'202':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
message: Scan started
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'500':
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/quotas/folders/{name}/usage:
parameters:
- name: name
in: path
description: folder name
required: true
schema:
type: string
- in: query
name: mode
required: false
description: the update mode specifies if the given quota usage values should be added or replace the current ones
schema:
type: string
enum:
- add
- reset
description: |
Update type:
* `add` - add the specified quota limits to the current used ones
* `reset` - reset the values to the specified ones. This is the default
example: reset
put:
tags:
- quota
summary: Update folder quota usage limits
description: Sets the current used quota limits for the given folder
operationId: folder_quota_update_usage
parameters:
- in: query
name: mode
required: false
description: the update mode specifies if the given quota usage values should be added or replace the current ones
schema:
type: string
enum:
- add
- reset
description: |
Update type:
* `add` - add the specified quota limits to the current used ones
* `reset` - reset the values to the specified ones. This is the default
example: reset
requestBody:
required: true
description: 'If used_quota_size and used_quota_files are missing they will default to 0, this means that if mode is "add" the current value, for the missing field, will remain unchanged, if mode is "reset" the missing field is set to 0'
content:
application/json:
schema:
$ref: '#/components/schemas/QuotaUsage'
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
message: Quota updated
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'500':
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/quota-scans:
get:
deprecated: true
tags:
- quota
summary: Get quota scans
description: Returns active user quota scans
operationId: get_quota_scans
description: Deprecated, please use '/quotas/users/scans' instead
operationId: get_users_quota_scans_deprecated
responses:
'200':
description: successful operation
@ -398,11 +750,12 @@ paths:
default:
$ref: '#/components/responses/DefaultResponse'
post:
deprecated: true
tags:
- quota
summary: Start user quota scan
description: Starts a new quota scan for the given user. A quota scan updates the number of files and their total size for the specified user and the virtual folders, if any, included in his quota
operationId: start_quota_scan
description: Deprecated, please use '/quotas/users/{username}/scan' instead
operationId: start_user_quota_scan_deprecated
requestBody:
required: true
content:
@ -434,11 +787,12 @@ paths:
$ref: '#/components/responses/DefaultResponse'
/quota-update:
put:
deprecated: true
tags:
- quota
summary: Update user quota limits
description: Sets the current used quota limits for the given user
operationId: quota_update
summary: Update quota usage limits
description: Deprecated, please use '/quotas/users/{username}/usage' instead
operationId: user_quota_update_usage_deprecated
parameters:
- in: query
name: mode
@ -478,19 +832,18 @@ paths:
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'500':
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/folder-quota-update:
put:
deprecated: true
tags:
- quota
summary: Update folder quota limits
description: Sets the current used quota limits for the given folder
operationId: folder_quota_update
description: Deprecated, please use '/quotas/folders/{name}/usage' instead
operationId: folder_quota_update_usage_deprecated
parameters:
- in: query
name: mode
@ -530,19 +883,18 @@ paths:
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'500':
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/folder-quota-scans:
get:
deprecated: true
tags:
- quota
summary: Get folders quota scans
description: Returns the active quota scans for folders
operationId: get_folders_quota_scans
description: Deprecated, please use '/quotas/folders/scans' instead
operationId: get_folders_quota_scans_deprecated
responses:
'200':
description: successful operation
@ -561,11 +913,12 @@ paths:
default:
$ref: '#/components/responses/DefaultResponse'
post:
deprecated: true
tags:
- quota
summary: Start folder quota scan
description: Starts a new quota scan for the given folder. A quota scan update the number of files and their total size for the specified folder
operationId: start_folder_quota_scan
summary: Start a folder quota scan
description: Deprecated, please use '/quotas/folders/{name}/scan' instead
operationId: start_folder_quota_scan_deprecated
requestBody:
required: true
content:
@ -2142,6 +2495,15 @@ components:
additional_info:
type: string
description: Free form text field
QuotaUsage:
type: object
properties:
used_quota_size:
type: integer
format: int64
used_quota_files:
type: integer
format: int32
Transfer:
type: object
properties:
@ -2223,6 +2585,20 @@ components:
type: integer
format: int64
description: scan start time as unix timestamp in milliseconds
DefenderEntry:
type: object
properties:
id:
type: string
ip:
type: string
score:
type: integer
description: the score increases whenever a violation is detected, such as an attempt to log in using an incorrect password or invalid username. If the score exceeds the configured threshold, the IP is banned. Omitted for banned IPs
ban_time:
type: string
format: date-time
description: date time until the IP is banned. For already banned hosts, the ban time is increased each time a new violation is detected. Omitted if the IP is not banned
SSHHostKey:
type: object
properties:

View file

@ -580,10 +580,14 @@ func (s *httpdServer) initializeRouter() {
router.With(checkPerm(dataprovider.PermAdminCloseConnections)).
Delete(activeConnectionsPath+"/{connectionID}", handleCloseConnection)
router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotaScanPath, getQuotaScans)
router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotaScanPath, startQuotaScan)
router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotaScanVFolderPath, getVFolderQuotaScans)
router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotaScanVFolderPath, startVFolderQuotaScan)
router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotaScanPath, getUsersQuotaScans)
router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotasBasePath+"/users/scans", getUsersQuotaScans)
router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotaScanPath, startUserQuotaScanCompat)
router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotasBasePath+"/users/{username}/scan", startUserQuotaScan)
router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotaScanVFolderPath, getFoldersQuotaScans)
router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotasBasePath+"/folders/scans", getFoldersQuotaScans)
router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotaScanVFolderPath, startFolderQuotaScanCompat)
router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotasBasePath+"/folders/{name}/scan", startFolderQuotaScan)
router.With(checkPerm(dataprovider.PermAdminViewUsers)).Get(userPath, getUsers)
router.With(checkPerm(dataprovider.PermAdminAddUsers)).Post(userPath, addUser)
router.With(checkPerm(dataprovider.PermAdminViewUsers)).Get(userPath+"/{username}", getUserByUsername)
@ -597,8 +601,13 @@ func (s *httpdServer) initializeRouter() {
router.With(checkPerm(dataprovider.PermAdminManageSystem)).Get(dumpDataPath, dumpData)
router.With(checkPerm(dataprovider.PermAdminManageSystem)).Get(loadDataPath, loadData)
router.With(checkPerm(dataprovider.PermAdminManageSystem)).Post(loadDataPath, loadDataFromRequest)
router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Put(updateUsedQuotaPath, updateUserQuotaUsage)
router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Put(updateFolderUsedQuotaPath, updateVFolderQuotaUsage)
router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Put(updateUsedQuotaPath, updateUserQuotaUsageCompat)
router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+"/users/{username}/usage", updateUserQuotaUsage)
router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Put(updateFolderUsedQuotaPath, updateFolderQuotaUsageCompat)
router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+"/folders/{name}/usage", updateFolderQuotaUsage)
router.With(checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderHosts, getDefenderHosts)
router.With(checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderHosts+"/{id}", getDefenderHostByID)
router.With(checkPerm(dataprovider.PermAdminManageDefender)).Delete(defenderHosts+"/{id}", deleteDefenderHostByID)
router.With(checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderBanTime, getBanTime)
router.With(checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderScore, getScore)
router.With(checkPerm(dataprovider.PermAdminManageDefender)).Post(defenderUnban, unban)
@ -719,11 +728,11 @@ func (s *httpdServer) initializeRouter() {
router.With(checkPerm(dataprovider.PermAdminDeleteUsers), verifyCSRFHeader).
Delete(webFolderPath+"/{name}", deleteFolder)
router.With(checkPerm(dataprovider.PermAdminQuotaScans), verifyCSRFHeader).
Post(webScanVFolderPath, startVFolderQuotaScan)
Post(webScanVFolderPath+"/{name}", startFolderQuotaScan)
router.With(checkPerm(dataprovider.PermAdminDeleteUsers), verifyCSRFHeader).
Delete(webUserPath+"/{username}", deleteUser)
router.With(checkPerm(dataprovider.PermAdminQuotaScans), verifyCSRFHeader).
Post(webQuotaScanPath, startQuotaScan)
Post(webQuotaScanPath+"/{username}", startUserQuotaScan)
router.With(checkPerm(dataprovider.PermAdminManageSystem)).Get(webMaintenancePath, handleWebMaintenance)
router.With(checkPerm(dataprovider.PermAdminManageSystem)).Get(webBackupPath, dumpData)
router.With(checkPerm(dataprovider.PermAdminManageSystem)).Post(webRestorePath, handleWebRestore)

View file

@ -3,6 +3,7 @@ package httpdtest
import (
"bytes"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
@ -26,23 +27,23 @@ import (
)
const (
tokenPath = "/api/v2/token"
activeConnectionsPath = "/api/v2/connections"
quotaScanPath = "/api/v2/quota-scans"
quotaScanVFolderPath = "/api/v2/folder-quota-scans"
userPath = "/api/v2/users"
versionPath = "/api/v2/version"
folderPath = "/api/v2/folders"
serverStatusPath = "/api/v2/status"
dumpDataPath = "/api/v2/dumpdata"
loadDataPath = "/api/v2/loaddata"
updateUsedQuotaPath = "/api/v2/quota-update"
updateFolderUsedQuotaPath = "/api/v2/folder-quota-update"
defenderBanTime = "/api/v2/defender/bantime"
defenderUnban = "/api/v2/defender/unban"
defenderScore = "/api/v2/defender/score"
adminPath = "/api/v2/admins"
adminPwdPath = "/api/v2/admin/changepwd"
tokenPath = "/api/v2/token"
activeConnectionsPath = "/api/v2/connections"
quotasBasePath = "/api/v2/quotas"
quotaScanPath = "/api/v2/quotas/users/scans"
quotaScanVFolderPath = "/api/v2/quotas/folders/scans"
userPath = "/api/v2/users"
versionPath = "/api/v2/version"
folderPath = "/api/v2/folders"
serverStatusPath = "/api/v2/status"
dumpDataPath = "/api/v2/dumpdata"
loadDataPath = "/api/v2/loaddata"
defenderHosts = "/api/v2/defender/hosts"
defenderBanTime = "/api/v2/defender/bantime"
defenderUnban = "/api/v2/defender/unban"
defenderScore = "/api/v2/defender/score"
adminPath = "/api/v2/admins"
adminPwdPath = "/api/v2/admin/changepwd"
)
const (
@ -392,9 +393,8 @@ func GetQuotaScans(expectedStatusCode int) ([]common.ActiveQuotaScan, []byte, er
// StartQuotaScan starts a new quota scan for the given user and checks the received HTTP Status code against expectedStatusCode.
func StartQuotaScan(user dataprovider.User, expectedStatusCode int) ([]byte, error) {
var body []byte
userAsJSON, _ := json.Marshal(user)
resp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(quotaScanPath), bytes.NewBuffer(userAsJSON),
"", getDefaultToken())
resp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(quotasBasePath, "users", user.Username, "scan"),
nil, "", getDefaultToken())
if err != nil {
return body, err
}
@ -407,7 +407,7 @@ func StartQuotaScan(user dataprovider.User, expectedStatusCode int) ([]byte, err
func UpdateQuotaUsage(user dataprovider.User, mode string, expectedStatusCode int) ([]byte, error) {
var body []byte
userAsJSON, _ := json.Marshal(user)
url, err := addModeQueryParam(buildURLRelativeToBase(updateUsedQuotaPath), mode)
url, err := addModeQueryParam(buildURLRelativeToBase(quotasBasePath, "users", user.Username, "usage"), mode)
if err != nil {
return body, err
}
@ -584,9 +584,8 @@ func GetFoldersQuotaScans(expectedStatusCode int) ([]common.ActiveVirtualFolderQ
// StartFolderQuotaScan start a new quota scan for the given folder and checks the received HTTP Status code against expectedStatusCode.
func StartFolderQuotaScan(folder vfs.BaseVirtualFolder, expectedStatusCode int) ([]byte, error) {
var body []byte
folderAsJSON, _ := json.Marshal(folder)
resp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(quotaScanVFolderPath),
bytes.NewBuffer(folderAsJSON), "", getDefaultToken())
resp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(quotasBasePath, "folders", folder.Name, "scan"),
nil, "", getDefaultToken())
if err != nil {
return body, err
}
@ -599,7 +598,7 @@ func StartFolderQuotaScan(folder vfs.BaseVirtualFolder, expectedStatusCode int)
func UpdateFolderQuotaUsage(folder vfs.BaseVirtualFolder, mode string, expectedStatusCode int) ([]byte, error) {
var body []byte
folderAsJSON, _ := json.Marshal(folder)
url, err := addModeQueryParam(buildURLRelativeToBase(updateFolderUsedQuotaPath), mode)
url, err := addModeQueryParam(buildURLRelativeToBase(quotasBasePath, "folders", folder.Name, "usage"), mode)
if err != nil {
return body, err
}
@ -648,6 +647,61 @@ func GetStatus(expectedStatusCode int) (httpd.ServicesStatus, []byte, error) {
return response, body, err
}
// GetDefenderHosts returns hosts that are banned or for which some violations have been detected
func GetDefenderHosts(expectedStatusCode int) ([]common.DefenderEntry, []byte, error) {
var response []common.DefenderEntry
var body []byte
url, err := url.Parse(buildURLRelativeToBase(defenderHosts))
if err != nil {
return response, body, err
}
resp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, "", getDefaultToken())
if err != nil {
return response, body, err
}
defer resp.Body.Close()
err = checkResponse(resp.StatusCode, expectedStatusCode)
if err == nil && expectedStatusCode == http.StatusOK {
err = render.DecodeJSON(resp.Body, &response)
} else {
body, _ = getResponseBody(resp)
}
return response, body, err
}
// GetDefenderHostByIP returns the host with the given IP, if it exists
func GetDefenderHostByIP(ip string, expectedStatusCode int) (common.DefenderEntry, []byte, error) {
var host common.DefenderEntry
var body []byte
id := hex.EncodeToString([]byte(ip))
resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(defenderHosts, id),
nil, "", getDefaultToken())
if err != nil {
return host, body, err
}
defer resp.Body.Close()
err = checkResponse(resp.StatusCode, expectedStatusCode)
if err == nil && expectedStatusCode == http.StatusOK {
err = render.DecodeJSON(resp.Body, &host)
} else {
body, _ = getResponseBody(resp)
}
return host, body, err
}
// RemoveDefenderHostByIP removes the host with the given IP from the defender list
func RemoveDefenderHostByIP(ip string, expectedStatusCode int) ([]byte, error) {
var body []byte
id := hex.EncodeToString([]byte(ip))
resp, err := sendHTTPRequest(http.MethodDelete, buildURLRelativeToBase(defenderHosts, id), nil, "", getDefaultToken())
if err != nil {
return body, err
}
defer resp.Body.Close()
body, _ = getResponseBody(resp)
return body, checkResponse(resp.StatusCode, expectedStatusCode)
}
// GetBanTime returns the ban time for the given IP address
func GetBanTime(ip string, expectedStatusCode int) (map[string]interface{}, []byte, error) {
var response map[string]interface{}

View file

@ -179,13 +179,11 @@ function deleteAction() {
action: function (e, dt, node, config) {
dt.button('quota_scan:name').enable(false);
var folderName = dt.row({ selected: true }).data()[0];
var path = '{{.FolderQuotaScanURL}}'
var path = '{{.FolderQuotaScanURL}}'+ "/" + fixedEncodeURIComponent(folderName);
$.ajax({
url: path,
type: 'POST',
dataType: 'json',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({ "name": folderName }),
timeout: 15000,
success: function (result) {
dt.button('quota_scan:name').enable(true);

View file

@ -199,13 +199,11 @@
action: function (e, dt, node, config) {
dt.button('quota_scan:name').enable(false);
var username = dt.row({ selected: true }).data()[1];
var path = '{{.QuotaScanURL}}'
var path = '{{.QuotaScanURL}}'+ "/" + fixedEncodeURIComponent(username);
$.ajax({
url: path,
type: 'POST',
dataType: 'json',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({ "username": username }),
timeout: 15000,
success: function (result) {
dt.button('quota_scan:name').enable(true);