mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-21 15:10:23 +00:00
parent
65948a47f1
commit
da5a061b65
24 changed files with 1218 additions and 80 deletions
|
@ -18,7 +18,7 @@ Several storage backends are supported: local filesystem, encrypted local filesy
|
|||
- SQLite, MySQL, PostgreSQL, CockroachDB, Bolt (key/value store in pure Go) and in-memory data providers are supported.
|
||||
- Chroot isolation for local accounts. Cloud-based accounts can be restricted to a certain base path.
|
||||
- Per user and per directory virtual permissions, for each exposed path you can allow or deny: directory listing, upload, overwrite, download, delete, rename, create directories, create symlinks, change owner/group/file mode.
|
||||
- [REST API](./docs/rest-api.md) for users and folders management, backup, restore and real time reports of the active connections with possibility of forcibly closing a connection.
|
||||
- [REST API](./docs/rest-api.md) for users and folders management, data retention, backup, restore and real time reports of the active connections with possibility of forcibly closing a connection.
|
||||
- [Web based administration interface](./docs/web-admin.md) to easily manage users, folders and connections.
|
||||
- [Web client interface](./docs/web-client.md) so that end users can change their credentials and browse their files.
|
||||
- Public key and password authentication. Multiple public keys per user are supported.
|
||||
|
|
|
@ -887,8 +887,8 @@ type ActiveVirtualFolderQuotaScan struct {
|
|||
// ActiveScans holds the active quota scans
|
||||
type ActiveScans struct {
|
||||
sync.RWMutex
|
||||
UserHomeScans []ActiveQuotaScan
|
||||
FolderScans []ActiveVirtualFolderQuotaScan
|
||||
UserScans []ActiveQuotaScan
|
||||
FolderScans []ActiveVirtualFolderQuotaScan
|
||||
}
|
||||
|
||||
// GetUsersQuotaScans returns the active quota scans for users home directories
|
||||
|
@ -896,8 +896,8 @@ func (s *ActiveScans) GetUsersQuotaScans() []ActiveQuotaScan {
|
|||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
scans := make([]ActiveQuotaScan, len(s.UserHomeScans))
|
||||
copy(scans, s.UserHomeScans)
|
||||
scans := make([]ActiveQuotaScan, len(s.UserScans))
|
||||
copy(scans, s.UserScans)
|
||||
return scans
|
||||
}
|
||||
|
||||
|
@ -907,12 +907,12 @@ func (s *ActiveScans) AddUserQuotaScan(username string) bool {
|
|||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
for _, scan := range s.UserHomeScans {
|
||||
for _, scan := range s.UserScans {
|
||||
if scan.Username == username {
|
||||
return false
|
||||
}
|
||||
}
|
||||
s.UserHomeScans = append(s.UserHomeScans, ActiveQuotaScan{
|
||||
s.UserScans = append(s.UserScans, ActiveQuotaScan{
|
||||
Username: username,
|
||||
StartTime: util.GetTimeAsMsSinceEpoch(time.Now()),
|
||||
})
|
||||
|
@ -925,18 +925,15 @@ func (s *ActiveScans) RemoveUserQuotaScan(username string) bool {
|
|||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
indexToRemove := -1
|
||||
for i, scan := range s.UserHomeScans {
|
||||
for idx, scan := range s.UserScans {
|
||||
if scan.Username == username {
|
||||
indexToRemove = i
|
||||
break
|
||||
lastIdx := len(s.UserScans) - 1
|
||||
s.UserScans[idx] = s.UserScans[lastIdx]
|
||||
s.UserScans = s.UserScans[:lastIdx]
|
||||
return true
|
||||
}
|
||||
}
|
||||
if indexToRemove >= 0 {
|
||||
s.UserHomeScans[indexToRemove] = s.UserHomeScans[len(s.UserHomeScans)-1]
|
||||
s.UserHomeScans = s.UserHomeScans[:len(s.UserHomeScans)-1]
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -973,17 +970,14 @@ func (s *ActiveScans) RemoveVFolderQuotaScan(folderName string) bool {
|
|||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
indexToRemove := -1
|
||||
for i, scan := range s.FolderScans {
|
||||
for idx, scan := range s.FolderScans {
|
||||
if scan.Name == folderName {
|
||||
indexToRemove = i
|
||||
break
|
||||
lastIdx := len(s.FolderScans) - 1
|
||||
s.FolderScans[idx] = s.FolderScans[lastIdx]
|
||||
s.FolderScans = s.FolderScans[:lastIdx]
|
||||
return true
|
||||
}
|
||||
}
|
||||
if indexToRemove >= 0 {
|
||||
s.FolderScans[indexToRemove] = s.FolderScans[len(s.FolderScans)-1]
|
||||
s.FolderScans = s.FolderScans[:len(s.FolderScans)-1]
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -535,13 +535,18 @@ func TestQuotaScans(t *testing.T) {
|
|||
username := "username"
|
||||
assert.True(t, QuotaScans.AddUserQuotaScan(username))
|
||||
assert.False(t, QuotaScans.AddUserQuotaScan(username))
|
||||
if assert.Len(t, QuotaScans.GetUsersQuotaScans(), 1) {
|
||||
assert.Equal(t, QuotaScans.GetUsersQuotaScans()[0].Username, username)
|
||||
usersScans := QuotaScans.GetUsersQuotaScans()
|
||||
if assert.Len(t, usersScans, 1) {
|
||||
assert.Equal(t, usersScans[0].Username, username)
|
||||
assert.Equal(t, QuotaScans.UserScans[0].StartTime, usersScans[0].StartTime)
|
||||
QuotaScans.UserScans[0].StartTime = 0
|
||||
assert.NotEqual(t, QuotaScans.UserScans[0].StartTime, usersScans[0].StartTime)
|
||||
}
|
||||
|
||||
assert.True(t, QuotaScans.RemoveUserQuotaScan(username))
|
||||
assert.False(t, QuotaScans.RemoveUserQuotaScan(username))
|
||||
assert.Len(t, QuotaScans.GetUsersQuotaScans(), 0)
|
||||
assert.Len(t, usersScans, 1)
|
||||
|
||||
folderName := "folder"
|
||||
assert.True(t, QuotaScans.AddVFolderQuotaScan(folderName))
|
||||
|
|
269
common/dataretention.go
Normal file
269
common/dataretention.go
Normal file
|
@ -0,0 +1,269 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/dataprovider"
|
||||
"github.com/drakkan/sftpgo/v2/logger"
|
||||
"github.com/drakkan/sftpgo/v2/util"
|
||||
)
|
||||
|
||||
var (
|
||||
// RetentionChecks is the list of active quota scans
|
||||
RetentionChecks ActiveRetentionChecks
|
||||
)
|
||||
|
||||
// ActiveRetentionChecks holds the active quota scans
|
||||
type ActiveRetentionChecks struct {
|
||||
sync.RWMutex
|
||||
Checks []RetentionCheck
|
||||
}
|
||||
|
||||
// Get returns the active retention checks
|
||||
func (c *ActiveRetentionChecks) Get() []RetentionCheck {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
|
||||
checks := make([]RetentionCheck, 0, len(c.Checks))
|
||||
for _, check := range c.Checks {
|
||||
foldersCopy := make([]FolderRetention, len(check.Folders))
|
||||
copy(foldersCopy, check.Folders)
|
||||
checks = append(checks, RetentionCheck{
|
||||
Username: check.Username,
|
||||
StartTime: check.StartTime,
|
||||
Folders: foldersCopy,
|
||||
})
|
||||
}
|
||||
return checks
|
||||
}
|
||||
|
||||
// Add a new retention check, returns nil if a retention check for the given
|
||||
// username is already active. The returned result can be used to start the check
|
||||
func (c *ActiveRetentionChecks) Add(check RetentionCheck, user *dataprovider.User) *RetentionCheck {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
for _, val := range c.Checks {
|
||||
if val.Username == user.Username {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// we silently ignore file patterns
|
||||
user.Filters.FilePatterns = nil
|
||||
conn := NewBaseConnection("", "", "", "", *user)
|
||||
conn.ID = fmt.Sprintf("retention_check_%v", user.Username)
|
||||
check.Username = user.Username
|
||||
check.StartTime = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
check.conn = conn
|
||||
check.updateUserPermissions()
|
||||
c.Checks = append(c.Checks, check)
|
||||
|
||||
return &check
|
||||
}
|
||||
|
||||
// remove a user from the ones with active retention checks
|
||||
// and returns true if the user is removed
|
||||
func (c *ActiveRetentionChecks) remove(username string) bool {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
for idx, check := range c.Checks {
|
||||
if check.Username == username {
|
||||
lastIdx := len(c.Checks) - 1
|
||||
c.Checks[idx] = c.Checks[lastIdx]
|
||||
c.Checks = c.Checks[:lastIdx]
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// FolderRetention defines the retention policy for the specified directory path
|
||||
type FolderRetention struct {
|
||||
// Path is the exposed virtual directory path, if no other specific retention is defined,
|
||||
// the retention applies for sub directories too. For example if retention is defined
|
||||
// for the paths "/" and "/sub" then the retention for "/" is applied for any file outside
|
||||
// the "/sub" directory
|
||||
Path string `json:"path"`
|
||||
// Retention time in hours. 0 means exclude this path
|
||||
Retention int `json:"retention"`
|
||||
// DeleteEmptyDirs defines if empty directories will be deleted.
|
||||
// The user need the delete permission
|
||||
DeleteEmptyDirs bool `json:"delete_empty_dirs,omitempty"`
|
||||
// IgnoreUserPermissions defines if delete files even if the user does not have the delete permission.
|
||||
// The default is "false" which means that files will be skipped if the user does not have the permission
|
||||
// to delete them. This applies to sub directories too.
|
||||
IgnoreUserPermissions bool `json:"ignore_user_permissions,omitempty"`
|
||||
}
|
||||
|
||||
func (f *FolderRetention) isValid() error {
|
||||
f.Path = path.Clean(f.Path)
|
||||
if !path.IsAbs(f.Path) {
|
||||
return util.NewValidationError(fmt.Sprintf("folder retention: invalid path %#v, please specify an absolute POSIX path",
|
||||
f.Path))
|
||||
}
|
||||
if f.Retention < 0 {
|
||||
return util.NewValidationError(fmt.Sprintf("invalid folder retention %v, it must be greater or equal to zero",
|
||||
f.Retention))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RetentionCheck defines an active retention check
|
||||
type RetentionCheck struct {
|
||||
// Username to which the retention check refers
|
||||
Username string `json:"username"`
|
||||
// retention check start time as unix timestamp in milliseconds
|
||||
StartTime int64 `json:"start_time"`
|
||||
// affected folders
|
||||
Folders []FolderRetention `json:"folders"`
|
||||
// Cleanup results
|
||||
conn *BaseConnection
|
||||
}
|
||||
|
||||
// Validate returns an error if the specified folders are not valid
|
||||
func (c *RetentionCheck) Validate() error {
|
||||
folderPaths := make(map[string]bool)
|
||||
nothingToDo := true
|
||||
for idx := range c.Folders {
|
||||
f := &c.Folders[idx]
|
||||
if err := f.isValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
if f.Retention > 0 {
|
||||
nothingToDo = false
|
||||
}
|
||||
if _, ok := folderPaths[f.Path]; ok {
|
||||
return util.NewValidationError(fmt.Sprintf("duplicated folder path %#v", f.Path))
|
||||
}
|
||||
folderPaths[f.Path] = true
|
||||
}
|
||||
if nothingToDo {
|
||||
return util.NewValidationError("nothing to delete!")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *RetentionCheck) updateUserPermissions() {
|
||||
for _, folder := range c.Folders {
|
||||
if folder.IgnoreUserPermissions {
|
||||
c.conn.User.Permissions[folder.Path] = []string{dataprovider.PermAny}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *RetentionCheck) getFolderRetention(folderPath string) (FolderRetention, error) {
|
||||
dirsForPath := util.GetDirsForVirtualPath(folderPath)
|
||||
for _, dirPath := range dirsForPath {
|
||||
for _, folder := range c.Folders {
|
||||
if folder.Path == dirPath {
|
||||
return folder, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FolderRetention{}, fmt.Errorf("unable to find folder retention for %#v", folderPath)
|
||||
}
|
||||
|
||||
func (c *RetentionCheck) removeFile(virtualPath string, info os.FileInfo) error {
|
||||
fs, fsPath, err := c.conn.GetFsAndResolvedPath(virtualPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.conn.RemoveFile(fs, fsPath, virtualPath, info)
|
||||
}
|
||||
|
||||
func (c *RetentionCheck) cleanupFolder(folderPath string) error {
|
||||
cleanupPerms := []string{dataprovider.PermListItems, dataprovider.PermDelete}
|
||||
if !c.conn.User.HasPerms(cleanupPerms, folderPath) {
|
||||
c.conn.Log(logger.LevelInfo, "user %#v does not have permissions to check retention on %#v, retention check skipped",
|
||||
c.conn.User, folderPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
folderRetention, err := c.getFolderRetention(folderPath)
|
||||
if err != nil {
|
||||
c.conn.Log(logger.LevelError, "unable to get folder retention for path %#v", folderPath)
|
||||
return err
|
||||
}
|
||||
if folderRetention.Retention == 0 {
|
||||
c.conn.Log(logger.LevelDebug, "retention check skipped for folder %#v, retention is set to 0", folderPath)
|
||||
return nil
|
||||
}
|
||||
c.conn.Log(logger.LevelDebug, "start retention check for folder %#v, retention: %v hours, delete empty dirs? %v, ignore user perms? %v",
|
||||
folderPath, folderRetention.Retention, folderRetention.DeleteEmptyDirs, folderRetention.IgnoreUserPermissions)
|
||||
files, err := c.conn.ListDir(folderPath)
|
||||
if err != nil {
|
||||
if err == c.conn.GetNotExistError() {
|
||||
c.conn.Log(logger.LevelDebug, "folder %#v does not exist, retention check skipped", folderPath)
|
||||
return nil
|
||||
}
|
||||
c.conn.Log(logger.LevelWarn, "unable to list directory %#v", folderPath)
|
||||
return err
|
||||
}
|
||||
deletedFiles := 0
|
||||
deletedSize := int64(0)
|
||||
for _, info := range files {
|
||||
virtualPath := path.Join(folderPath, info.Name())
|
||||
if info.IsDir() {
|
||||
if err := c.cleanupFolder(virtualPath); err != nil {
|
||||
c.conn.Log(logger.LevelWarn, "unable to cleanup folder %#v: %v", virtualPath, err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
retentionTime := info.ModTime().Add(time.Duration(folderRetention.Retention) * time.Hour)
|
||||
if retentionTime.Before(time.Now()) {
|
||||
if err := c.removeFile(virtualPath, info); err != nil {
|
||||
c.conn.Log(logger.LevelWarn, "unable to remove file %#v, retention %v: %v",
|
||||
virtualPath, retentionTime, err)
|
||||
return err
|
||||
}
|
||||
c.conn.Log(logger.LevelDebug, "removed file %#v, modification time: %v, retention: %v hours, retention time: %v",
|
||||
virtualPath, info.ModTime(), folderRetention.Retention, retentionTime)
|
||||
deletedFiles++
|
||||
deletedSize += info.Size()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if folderRetention.DeleteEmptyDirs {
|
||||
c.checkEmptyDirRemoval(folderPath)
|
||||
}
|
||||
c.conn.Log(logger.LevelDebug, "retention check completed for folder %#v, deleted files: %v, deleted size: %v bytes",
|
||||
folderPath, deletedFiles, deletedSize)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *RetentionCheck) checkEmptyDirRemoval(folderPath string) {
|
||||
if folderPath != "/" && c.conn.User.HasPerm(dataprovider.PermDelete, path.Dir(folderPath)) {
|
||||
files, err := c.conn.ListDir(folderPath)
|
||||
if err == nil && len(files) == 0 {
|
||||
err = c.conn.RemoveDir(folderPath)
|
||||
c.conn.Log(logger.LevelDebug, "tryed to remove empty dir %#v, error: %v", folderPath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the retention check
|
||||
func (c *RetentionCheck) Start() {
|
||||
c.conn.Log(logger.LevelInfo, "retention check started")
|
||||
defer RetentionChecks.remove(c.conn.User.Username)
|
||||
defer c.conn.CloseFS() //nolint:errcheck
|
||||
|
||||
for _, folder := range c.Folders {
|
||||
if folder.Retention > 0 {
|
||||
if err := c.cleanupFolder(folder.Path); err != nil {
|
||||
c.conn.Log(logger.LevelWarn, "retention check failed, unable to cleanup folder %#v", folder.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.conn.Log(logger.LevelInfo, "retention check completed")
|
||||
}
|
187
common/dataretention_test.go
Normal file
187
common/dataretention_test.go
Normal file
|
@ -0,0 +1,187 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/dataprovider"
|
||||
"github.com/drakkan/sftpgo/v2/sdk"
|
||||
)
|
||||
|
||||
func TestRetentionValidation(t *testing.T) {
|
||||
check := RetentionCheck{}
|
||||
check.Folders = append(check.Folders, FolderRetention{
|
||||
Path: "relative",
|
||||
Retention: 10,
|
||||
})
|
||||
err := check.Validate()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "please specify an absolute POSIX path")
|
||||
|
||||
check.Folders = []FolderRetention{
|
||||
{
|
||||
Path: "/",
|
||||
Retention: -1,
|
||||
},
|
||||
}
|
||||
err = check.Validate()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid folder retention")
|
||||
|
||||
check.Folders = []FolderRetention{
|
||||
{
|
||||
Path: "/ab/..",
|
||||
Retention: 0,
|
||||
},
|
||||
}
|
||||
err = check.Validate()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "nothing to delete")
|
||||
assert.Equal(t, "/", check.Folders[0].Path)
|
||||
|
||||
check.Folders = append(check.Folders, FolderRetention{
|
||||
Path: "/../..",
|
||||
Retention: 24,
|
||||
})
|
||||
err = check.Validate()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), `duplicated folder path "/"`)
|
||||
|
||||
check.Folders = []FolderRetention{
|
||||
{
|
||||
Path: "/dir1",
|
||||
Retention: 48,
|
||||
},
|
||||
{
|
||||
Path: "/dir2",
|
||||
Retention: 96,
|
||||
},
|
||||
}
|
||||
err = check.Validate()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRetentionPermissionsAndGetFolder(t *testing.T) {
|
||||
user := dataprovider.User{
|
||||
BaseUser: sdk.BaseUser{
|
||||
Username: "user1",
|
||||
},
|
||||
}
|
||||
user.Permissions = make(map[string][]string)
|
||||
user.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermDelete}
|
||||
user.Permissions["/dir1"] = []string{dataprovider.PermListItems}
|
||||
user.Permissions["/dir2/sub1"] = []string{dataprovider.PermCreateDirs}
|
||||
user.Permissions["/dir2/sub2"] = []string{dataprovider.PermDelete}
|
||||
|
||||
check := RetentionCheck{
|
||||
Folders: []FolderRetention{
|
||||
{
|
||||
Path: "/dir2",
|
||||
Retention: 24 * 7,
|
||||
IgnoreUserPermissions: true,
|
||||
},
|
||||
{
|
||||
Path: "/dir3",
|
||||
Retention: 24 * 7,
|
||||
IgnoreUserPermissions: false,
|
||||
},
|
||||
{
|
||||
Path: "/dir2/sub1/sub",
|
||||
Retention: 24,
|
||||
IgnoreUserPermissions: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
conn := NewBaseConnection("", "", "", "", user)
|
||||
conn.ID = fmt.Sprintf("retention_check_%v", user.Username)
|
||||
check.conn = conn
|
||||
check.updateUserPermissions()
|
||||
assert.Equal(t, []string{dataprovider.PermListItems, dataprovider.PermDelete}, conn.User.Permissions["/"])
|
||||
assert.Equal(t, []string{dataprovider.PermListItems}, conn.User.Permissions["/dir1"])
|
||||
assert.Equal(t, []string{dataprovider.PermAny}, conn.User.Permissions["/dir2"])
|
||||
assert.Equal(t, []string{dataprovider.PermAny}, conn.User.Permissions["/dir2/sub1/sub"])
|
||||
assert.Equal(t, []string{dataprovider.PermCreateDirs}, conn.User.Permissions["/dir2/sub1"])
|
||||
assert.Equal(t, []string{dataprovider.PermDelete}, conn.User.Permissions["/dir2/sub2"])
|
||||
|
||||
_, err := check.getFolderRetention("/")
|
||||
assert.Error(t, err)
|
||||
folder, err := check.getFolderRetention("/dir3")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "/dir3", folder.Path)
|
||||
folder, err = check.getFolderRetention("/dir2/sub3")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "/dir2", folder.Path)
|
||||
folder, err = check.getFolderRetention("/dir2/sub2")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "/dir2", folder.Path)
|
||||
folder, err = check.getFolderRetention("/dir2/sub1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "/dir2", folder.Path)
|
||||
folder, err = check.getFolderRetention("/dir2/sub1/sub/sub")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "/dir2/sub1/sub", folder.Path)
|
||||
}
|
||||
|
||||
func TestRetentionCheckAddRemove(t *testing.T) {
|
||||
username := "username"
|
||||
user := dataprovider.User{
|
||||
BaseUser: sdk.BaseUser{
|
||||
Username: username,
|
||||
},
|
||||
}
|
||||
user.Permissions = make(map[string][]string)
|
||||
user.Permissions["/"] = []string{dataprovider.PermAny}
|
||||
check := RetentionCheck{
|
||||
Folders: []FolderRetention{
|
||||
{
|
||||
Path: "/",
|
||||
Retention: 48,
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.NotNil(t, RetentionChecks.Add(check, &user))
|
||||
checks := RetentionChecks.Get()
|
||||
require.Len(t, checks, 1)
|
||||
assert.Equal(t, username, checks[0].Username)
|
||||
assert.Greater(t, checks[0].StartTime, int64(0))
|
||||
require.Len(t, checks[0].Folders, 1)
|
||||
assert.Equal(t, check.Folders[0].Path, checks[0].Folders[0].Path)
|
||||
assert.Equal(t, check.Folders[0].Retention, checks[0].Folders[0].Retention)
|
||||
|
||||
assert.Nil(t, RetentionChecks.Add(check, &user))
|
||||
assert.True(t, RetentionChecks.remove(username))
|
||||
require.Len(t, RetentionChecks.Get(), 0)
|
||||
assert.False(t, RetentionChecks.remove(username))
|
||||
}
|
||||
|
||||
func TestCleanupErrors(t *testing.T) {
|
||||
user := dataprovider.User{
|
||||
BaseUser: sdk.BaseUser{
|
||||
Username: "u",
|
||||
},
|
||||
}
|
||||
user.Permissions = make(map[string][]string)
|
||||
user.Permissions["/"] = []string{dataprovider.PermAny}
|
||||
check := &RetentionCheck{
|
||||
Folders: []FolderRetention{
|
||||
{
|
||||
Path: "/path",
|
||||
Retention: 48,
|
||||
},
|
||||
},
|
||||
}
|
||||
check = RetentionChecks.Add(*check, &user)
|
||||
require.NotNil(t, check)
|
||||
|
||||
err := check.removeFile("missing file", nil)
|
||||
assert.Error(t, err)
|
||||
|
||||
err = check.cleanupFolder("/")
|
||||
assert.Error(t, err)
|
||||
|
||||
assert.True(t, RetentionChecks.remove(user.Username))
|
||||
}
|
|
@ -2325,6 +2325,225 @@ func TestGetQuotaError(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRetentionAPI(t *testing.T) {
|
||||
u := getTestUser()
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
uploadPath := path.Join(testDir, testFileName)
|
||||
|
||||
conn, client, err := getSftpClient(user)
|
||||
if assert.NoError(t, err) {
|
||||
defer conn.Close()
|
||||
defer client.Close()
|
||||
|
||||
err = client.Mkdir(testDir)
|
||||
assert.NoError(t, err)
|
||||
err = writeSFTPFile(uploadPath, 32, client)
|
||||
assert.NoError(t, err)
|
||||
|
||||
folderRetention := []common.FolderRetention{
|
||||
{
|
||||
Path: "/",
|
||||
Retention: 24,
|
||||
DeleteEmptyDirs: true,
|
||||
},
|
||||
}
|
||||
_, err = httpdtest.StartRetentionCheck(user.Username, folderRetention, http.StatusAccepted)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Eventually(t, func() bool {
|
||||
return len(common.RetentionChecks.Get()) == 0
|
||||
}, 1000*time.Millisecond, 50*time.Millisecond)
|
||||
|
||||
_, err = client.Stat(uploadPath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = client.Chtimes(uploadPath, time.Now().Add(-48*time.Hour), time.Now().Add(-48*time.Hour))
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = httpdtest.StartRetentionCheck(user.Username, folderRetention, http.StatusAccepted)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Eventually(t, func() bool {
|
||||
return len(common.RetentionChecks.Get()) == 0
|
||||
}, 1000*time.Millisecond, 50*time.Millisecond)
|
||||
|
||||
_, err = client.Stat(uploadPath)
|
||||
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||
|
||||
_, err = client.Stat(testDir)
|
||||
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||
|
||||
err = client.Mkdir(testDir)
|
||||
assert.NoError(t, err)
|
||||
err = writeSFTPFile(uploadPath, 32, client)
|
||||
assert.NoError(t, err)
|
||||
|
||||
folderRetention[0].DeleteEmptyDirs = false
|
||||
err = client.Chtimes(uploadPath, time.Now().Add(-48*time.Hour), time.Now().Add(-48*time.Hour))
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = httpdtest.StartRetentionCheck(user.Username, folderRetention, http.StatusAccepted)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Eventually(t, func() bool {
|
||||
return len(common.RetentionChecks.Get()) == 0
|
||||
}, 1000*time.Millisecond, 50*time.Millisecond)
|
||||
|
||||
_, err = client.Stat(uploadPath)
|
||||
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||
|
||||
_, err = client.Stat(testDir)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = writeSFTPFile(uploadPath, 32, client)
|
||||
assert.NoError(t, err)
|
||||
err = client.Chtimes(uploadPath, time.Now().Add(-48*time.Hour), time.Now().Add(-48*time.Hour))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// remove delete permissions to the user
|
||||
user.Permissions["/"+testDir] = []string{dataprovider.PermListItems, dataprovider.PermUpload,
|
||||
dataprovider.PermCreateDirs, dataprovider.PermChtimes}
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
conn, client, err = getSftpClient(user)
|
||||
if assert.NoError(t, err) {
|
||||
defer conn.Close()
|
||||
defer client.Close()
|
||||
|
||||
innerUploadFilePath := path.Join("/"+testDir, testDir, testFileName)
|
||||
err = client.Mkdir(path.Join(testDir, testDir))
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = writeSFTPFile(innerUploadFilePath, 32, client)
|
||||
assert.NoError(t, err)
|
||||
err = client.Chtimes(innerUploadFilePath, time.Now().Add(-48*time.Hour), time.Now().Add(-48*time.Hour))
|
||||
assert.NoError(t, err)
|
||||
|
||||
folderRetention := []common.FolderRetention{
|
||||
{
|
||||
Path: "/missing",
|
||||
Retention: 24,
|
||||
},
|
||||
{
|
||||
Path: "/" + testDir,
|
||||
Retention: 24,
|
||||
DeleteEmptyDirs: true,
|
||||
},
|
||||
{
|
||||
Path: path.Dir(innerUploadFilePath),
|
||||
Retention: 0,
|
||||
IgnoreUserPermissions: true,
|
||||
},
|
||||
}
|
||||
_, err = httpdtest.StartRetentionCheck(user.Username, folderRetention, http.StatusAccepted)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Eventually(t, func() bool {
|
||||
return len(common.RetentionChecks.Get()) == 0
|
||||
}, 1000*time.Millisecond, 50*time.Millisecond)
|
||||
|
||||
_, err = client.Stat(uploadPath)
|
||||
assert.NoError(t, err)
|
||||
_, err = client.Stat(innerUploadFilePath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
folderRetention[1].IgnoreUserPermissions = true
|
||||
_, err = httpdtest.StartRetentionCheck(user.Username, folderRetention, http.StatusAccepted)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Eventually(t, func() bool {
|
||||
return len(common.RetentionChecks.Get()) == 0
|
||||
}, 1000*time.Millisecond, 50*time.Millisecond)
|
||||
|
||||
_, err = client.Stat(uploadPath)
|
||||
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||
_, err = client.Stat(innerUploadFilePath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
folderRetention = []common.FolderRetention{
|
||||
|
||||
{
|
||||
Path: "/" + testDir,
|
||||
Retention: 24,
|
||||
DeleteEmptyDirs: true,
|
||||
IgnoreUserPermissions: true,
|
||||
},
|
||||
}
|
||||
|
||||
_, err = httpdtest.StartRetentionCheck(user.Username, folderRetention, http.StatusAccepted)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Eventually(t, func() bool {
|
||||
return len(common.RetentionChecks.Get()) == 0
|
||||
}, 1000*time.Millisecond, 50*time.Millisecond)
|
||||
|
||||
_, err = client.Stat(innerUploadFilePath)
|
||||
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||
}
|
||||
// finally test some errors removing files or folders
|
||||
if runtime.GOOS != osWindows {
|
||||
dirPath := filepath.Join(user.HomeDir, "adir", "sub")
|
||||
err := os.MkdirAll(dirPath, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
filePath := filepath.Join(dirPath, "f.dat")
|
||||
err = os.WriteFile(filePath, nil, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = os.Chtimes(filePath, time.Now().Add(-72*time.Hour), time.Now().Add(-72*time.Hour))
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = os.Chmod(dirPath, 0001)
|
||||
assert.NoError(t, err)
|
||||
|
||||
folderRetention := []common.FolderRetention{
|
||||
|
||||
{
|
||||
Path: "/adir",
|
||||
Retention: 24,
|
||||
DeleteEmptyDirs: true,
|
||||
IgnoreUserPermissions: true,
|
||||
},
|
||||
}
|
||||
|
||||
_, err = httpdtest.StartRetentionCheck(user.Username, folderRetention, http.StatusAccepted)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Eventually(t, func() bool {
|
||||
return len(common.RetentionChecks.Get()) == 0
|
||||
}, 1000*time.Millisecond, 50*time.Millisecond)
|
||||
|
||||
err = os.Chmod(dirPath, 0555)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = httpdtest.StartRetentionCheck(user.Username, folderRetention, http.StatusAccepted)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Eventually(t, func() bool {
|
||||
return len(common.RetentionChecks.Get()) == 0
|
||||
}, 1000*time.Millisecond, 50*time.Millisecond)
|
||||
|
||||
err = os.Chmod(dirPath, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = httpdtest.StartRetentionCheck(user.Username, folderRetention, http.StatusAccepted)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Eventually(t, func() bool {
|
||||
return len(common.RetentionChecks.Get()) == 0
|
||||
}, 1000*time.Millisecond, 50*time.Millisecond)
|
||||
|
||||
assert.NoDirExists(t, dirPath)
|
||||
}
|
||||
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRenameDir(t *testing.T) {
|
||||
u := getTestUser()
|
||||
testDir := "/dir-to-rename"
|
||||
|
|
|
@ -36,6 +36,7 @@ const (
|
|||
PermAdminManageSystem = "manage_system"
|
||||
PermAdminManageDefender = "manage_defender"
|
||||
PermAdminViewDefender = "view_defender"
|
||||
PermAdminRetentionChecks = "retention_checks"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -43,7 +44,7 @@ var (
|
|||
validAdminPerms = []string{PermAdminAny, PermAdminAddUsers, PermAdminChangeUsers, PermAdminDeleteUsers,
|
||||
PermAdminViewUsers, PermAdminViewConnections, PermAdminCloseConnections, PermAdminViewServerStatus,
|
||||
PermAdminManageAdmins, PermAdminManageAPIKeys, PermAdminQuotaScans, PermAdminManageSystem,
|
||||
PermAdminManageDefender, PermAdminViewDefender}
|
||||
PermAdminManageDefender, PermAdminViewDefender, PermAdminRetentionChecks}
|
||||
)
|
||||
|
||||
// TOTPConfig defines the time-based one time password configuration
|
||||
|
|
|
@ -8,7 +8,7 @@ The `secrets` section of the `kms` configuration allows to configure how to encr
|
|||
|
||||
- `url` defines the URI to the KMS service
|
||||
- `master_key`, defines the master encryption key as string. If not empty, it takes precedence over `master_key_path`.
|
||||
- `master_key_path` defines the absolute path to a file containing the master encryption key. This could be, for example, a docker secrets or a file protected with filesystem level permissions.
|
||||
- `master_key_path` defines the absolute path to a file containing the master encryption key. This could be, for example, a docker secret or a file protected with filesystem level permissions.
|
||||
|
||||
### Local provider
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# REST API
|
||||
|
||||
SFTPGo exposes REST API to manage, backup, and restore users and folders, and to get real time reports of the active connections with the ability to forcibly close a connection.
|
||||
SFTPGo exposes REST API to manage, backup, and restore users and folders, data retention, and to get real time reports of the active connections with the ability to forcibly close a connection.
|
||||
|
||||
If quota tracking is enabled in the configuration file, then the used size and number of files are updated each time a file is added/removed. If files are added/removed not using SFTP/SCP, or if you change `track_quota` from `2` to `1`, you can rescan the users home dir and update the used quota using the REST API.
|
||||
|
||||
|
@ -35,6 +35,7 @@ You can create other administrator and assign them the following permissions:
|
|||
- manage API keys
|
||||
- manage system
|
||||
- manage admins
|
||||
- manage data retention
|
||||
|
||||
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.
|
||||
|
||||
|
@ -62,6 +63,35 @@ API keys are not allowed for the following REST APIs:
|
|||
|
||||
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 data retention APIs allow you to define per-folder retention policies for each user. To clarify this concept let's show an example, a data retention check accepts a POST body like this one:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"path": "/folder1",
|
||||
"retention": 72
|
||||
},
|
||||
{
|
||||
"path": "/folder1/subfolder",
|
||||
"retention": 0
|
||||
},
|
||||
{
|
||||
"path": "/folder2",
|
||||
"retention": 24
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
In the above example we asked to SFTPGo:
|
||||
|
||||
- to delete all the files with modification time older than 72 hours in `/folder1`
|
||||
- to exclude `/folder1/subfolder`, no files will be deleted here
|
||||
- to delete all the files with modification time older than 24 hours in `/folder2`
|
||||
|
||||
You can find an example script that shows how to manage data retention [here](../examples/data-retention). Checks the REST API schema for full details.
|
||||
|
||||
:warning: Deleting files is an irreversible action, please make sure you fully understand what you are doing before using this feature, you may have users with overlapping home directories or virtual folders shared between multiple users, it is relatively easy to inadvertently delete files you need.
|
||||
|
||||
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/).
|
||||
|
|
34
examples/data-retention/README.md
Normal file
34
examples/data-retention/README.md
Normal file
|
@ -0,0 +1,34 @@
|
|||
# File retention policies
|
||||
|
||||
The `checkretention` example script shows how to use the SFTPGo REST API to manage data retention.
|
||||
|
||||
:warning: Deleting files is an irreversible action, please make sure you fully understand what you are doing before using this feature, you may have users with overlapping home directories or virtual folders shared between multiple users, it is relatively easy to inadvertently delete files you need.
|
||||
|
||||
The example shows how to setup a really simple retention policy, for each user it sends this request:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"path": "/",
|
||||
"retention": 168,
|
||||
"delete_empty_dirs": true,
|
||||
"ignore_user_permissions": false
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
so alls files with modification time older than 168 hours (7 days) will be deleted. Empty directories will be removed and the check will respect user's permissions, so if the user cannot delete a file/folder it will be skipped.
|
||||
|
||||
You can define different retention policies per-user and per-folder and you can exclude a folder setting the retention to `0`.
|
||||
|
||||
You can use this script as a starting point, please edit it according to your needs.
|
||||
|
||||
The script is written in Python and has the following requirements:
|
||||
|
||||
- python3 or python2
|
||||
- python [Requests](https://requests.readthedocs.io/en/master/) module
|
||||
|
||||
The provided example tries to connect to an SFTPGo instance running on `127.0.0.1:8080` using the following credentials:
|
||||
|
||||
- username: `admin`
|
||||
- password: `password`
|
115
examples/data-retention/checkretention
Executable file
115
examples/data-retention/checkretention
Executable file
|
@ -0,0 +1,115 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from datetime import datetime
|
||||
import sys
|
||||
import time
|
||||
|
||||
import pytz
|
||||
import requests
|
||||
|
||||
try:
|
||||
import urllib.parse as urlparse
|
||||
except ImportError:
|
||||
import urlparse
|
||||
|
||||
# change base_url to point to your SFTPGo installation
|
||||
base_url = "http://127.0.0.1:8080"
|
||||
# set to False if you want to skip TLS certificate validation
|
||||
verify_tls_cert = True
|
||||
# set the credentials for a valid admin here
|
||||
admin_user = "admin"
|
||||
admin_password = "password"
|
||||
|
||||
|
||||
class CheckRetention:
|
||||
|
||||
def __init__(self):
|
||||
self.limit = 100
|
||||
self.offset = 0
|
||||
self.access_token = ""
|
||||
self.access_token_expiration = None
|
||||
|
||||
def printLog(self, message):
|
||||
print("{} - {}".format(datetime.now(), message))
|
||||
|
||||
def checkAccessToken(self):
|
||||
if self.access_token != "" and self.access_token_expiration:
|
||||
expire_diff = self.access_token_expiration - datetime.now(tz=pytz.UTC)
|
||||
# we don't use total_seconds to be python 2 compatible
|
||||
seconds_to_expire = expire_diff.days * 86400 + expire_diff.seconds
|
||||
if seconds_to_expire > 180:
|
||||
return
|
||||
|
||||
auth = requests.auth.HTTPBasicAuth(admin_user, admin_password)
|
||||
r = requests.get(urlparse.urljoin(base_url, "api/v2/token"), auth=auth, verify=verify_tls_cert, timeout=10)
|
||||
if r.status_code != 200:
|
||||
self.printLog("error getting access token: {}".format(r.text))
|
||||
sys.exit(1)
|
||||
self.access_token = r.json()["access_token"]
|
||||
self.access_token_expiration = pytz.timezone("UTC").localize(datetime.strptime(r.json()["expires_at"],
|
||||
"%Y-%m-%dT%H:%M:%SZ"))
|
||||
|
||||
def getAuthHeader(self):
|
||||
self.checkAccessToken()
|
||||
return {"Authorization": "Bearer " + self.access_token}
|
||||
|
||||
def waitForRentionCheck(self, username):
|
||||
while True:
|
||||
auth_header = self.getAuthHeader()
|
||||
r = requests.get(urlparse.urljoin(base_url, "api/v2/retention/users/checks"), headers=auth_header, verify=verify_tls_cert,
|
||||
timeout=10)
|
||||
if r.status_code != 200:
|
||||
self.printLog("error getting retention checks while waiting for {}: {}".format(username, r.text))
|
||||
sys.exit(1)
|
||||
|
||||
checking = False
|
||||
for check in r.json():
|
||||
if check["username"] == username:
|
||||
checking = True
|
||||
if not checking:
|
||||
break
|
||||
self.printLog("waiting for the retention check to complete for user {}".format(username))
|
||||
time.sleep(2)
|
||||
|
||||
self.printLog("retention check for user {} finished".format(username))
|
||||
|
||||
def checkUserRetention(self, username):
|
||||
self.printLog("starting retention check for user {}".format(username))
|
||||
auth_header = self.getAuthHeader()
|
||||
retention = [
|
||||
{
|
||||
"path": "/",
|
||||
"retention": 168,
|
||||
"delete_empty_dirs": True,
|
||||
"ignore_user_permissions": False
|
||||
}
|
||||
]
|
||||
r = requests.post(urlparse.urljoin(base_url, "api/v2/retention/users/" + username + "/check"), headers=auth_header,
|
||||
json=retention, verify=verify_tls_cert, timeout=10)
|
||||
if r.status_code != 202:
|
||||
self.printLog("error starting retention check for user {}: {}".format(username, r.text))
|
||||
sys.exit(1)
|
||||
self.waitForRentionCheck(username)
|
||||
|
||||
def checkUsersRetention(self):
|
||||
while True:
|
||||
self.printLog("get users, limit {} offset {}".format(self.limit, self.offset))
|
||||
auth_header = self.getAuthHeader()
|
||||
payload = {"limit":self.limit, "offset":self.offset}
|
||||
r = requests.get(urlparse.urljoin(base_url, "api/v2/users"), headers=auth_header, params=payload,
|
||||
verify=verify_tls_cert, timeout=10)
|
||||
if r.status_code != 200:
|
||||
self.printLog("error getting users: {}".format(r.text))
|
||||
sys.exit(1)
|
||||
users = r.json()
|
||||
for user in users:
|
||||
self.checkUserRetention(user["username"])
|
||||
|
||||
self.offset += len(users)
|
||||
if len(users) < self.limit:
|
||||
break
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
c = CheckRetention()
|
||||
c.checkUsersRetention()
|
|
@ -6,7 +6,7 @@ The stored quota may be incorrect for several reasons, such as an unexpected shu
|
|||
|
||||
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.
|
||||
|
||||
If you want to track quotas, a scheduled quota scan is recommended. You could use this example as a starting point.
|
||||
If you want to track quotas, a scheduled quota scan is recommended. You can use this example as a starting point.
|
||||
|
||||
The script is written in Python and has the following requirements:
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ class UpdateQuota:
|
|||
def waitForQuotaUpdate(self, username):
|
||||
while True:
|
||||
auth_header = self.getAuthHeader()
|
||||
r = requests.get(urlparse.urljoin(base_url, "api/v2/quota-scans"), headers=auth_header, verify=verify_tls_cert,
|
||||
r = requests.get(urlparse.urljoin(base_url, "api/v2/quotas/users/scans"), headers=auth_header, verify=verify_tls_cert,
|
||||
timeout=10)
|
||||
if r.status_code != 200:
|
||||
self.printLog("error getting quota scans while waiting for {}: {}".format(username, r.text))
|
||||
|
@ -85,8 +85,8 @@ class UpdateQuota:
|
|||
def updateUserQuota(self, username):
|
||||
self.printLog("starting quota update for user {}".format(username))
|
||||
auth_header = self.getAuthHeader()
|
||||
r = requests.post(urlparse.urljoin(base_url, "api/v2/quota-scans"), headers=auth_header,
|
||||
json={"username":username}, verify=verify_tls_cert, timeout=10)
|
||||
r = requests.post(urlparse.urljoin(base_url, "api/v2/quotas/users/" + username + "/scan"), headers=auth_header,
|
||||
verify=verify_tls_cert, timeout=10)
|
||||
if r.status_code != 202:
|
||||
self.printLog("error starting quota scan for user {}: {}".format(username, r.text))
|
||||
sys.exit(1)
|
||||
|
|
22
go.mod
22
go.mod
|
@ -7,14 +7,14 @@ require (
|
|||
github.com/Azure/azure-storage-blob-go v0.14.0
|
||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
||||
github.com/alexedwards/argon2id v0.0.0-20210511081203-7d35d68092b8
|
||||
github.com/aws/aws-sdk-go v1.40.45
|
||||
github.com/aws/aws-sdk-go v1.40.49
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.1.1
|
||||
github.com/eikenb/pipeat v0.0.0-20210603033007-44fc3ffce52b
|
||||
github.com/fatih/color v1.12.0 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/fclairamb/ftpserverlib v0.16.0
|
||||
github.com/fclairamb/go-log v0.1.0
|
||||
github.com/go-chi/chi/v5 v5.0.4
|
||||
github.com/go-chi/jwtauth/v5 v5.0.1
|
||||
github.com/go-chi/jwtauth/v5 v5.0.2
|
||||
github.com/go-chi/render v1.0.1
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/golang/mock v1.6.0
|
||||
|
@ -51,7 +51,7 @@ require (
|
|||
github.com/shirou/gopsutil/v3 v3.21.8
|
||||
github.com/spf13/afero v1.6.0
|
||||
github.com/spf13/cobra v1.2.1
|
||||
github.com/spf13/viper v1.8.1
|
||||
github.com/spf13/viper v1.9.0
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/studio-b12/gowebdav v0.0.0-20210917133250-a3a86976a1df
|
||||
github.com/wagslane/go-password-validator v0.3.0
|
||||
|
@ -60,18 +60,18 @@ require (
|
|||
go.uber.org/automaxprocs v1.4.0
|
||||
gocloud.dev v0.24.0
|
||||
golang.org/x/crypto v0.0.0-20210915214749-c084706c2272
|
||||
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf
|
||||
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678
|
||||
golang.org/x/net v0.0.0-20210924151903-3ad01bbaa167
|
||||
golang.org/x/sys v0.0.0-20210925032602-92d5a993a665
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac
|
||||
google.golang.org/api v0.57.0
|
||||
google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4 // indirect
|
||||
google.golang.org/grpc v1.40.0
|
||||
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0 // indirect
|
||||
google.golang.org/grpc v1.41.0
|
||||
google.golang.org/protobuf v1.27.1
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.94.1 // indirect
|
||||
cloud.google.com/go v0.95.0 // indirect
|
||||
github.com/Azure/azure-pipeline-go v0.2.3 // indirect
|
||||
github.com/StackExchange/wmi v1.2.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
|
@ -100,7 +100,7 @@ require (
|
|||
github.com/lestrrat-go/iter v1.0.1 // indirect
|
||||
github.com/lestrrat-go/option v1.0.0 // indirect
|
||||
github.com/magiconair/properties v1.8.5 // indirect
|
||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||
github.com/mattn/go-colorable v0.1.10 // indirect
|
||||
github.com/mattn/go-ieproxy v0.0.1 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/minio/sha256-simd v1.0.0 // indirect
|
||||
|
@ -130,5 +130,5 @@ replace (
|
|||
github.com/eikenb/pipeat => github.com/drakkan/pipeat v0.0.0-20210805162858-70e57fa8a639
|
||||
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
|
||||
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20210918082254-e7eb8487714b
|
||||
golang.org/x/net => github.com/drakkan/net v0.0.0-20210918081947-af83c6eab079
|
||||
golang.org/x/net => github.com/drakkan/net v0.0.0-20210925100637-0aaabc6c2a0b
|
||||
)
|
||||
|
|
54
go.sum
54
go.sum
|
@ -31,8 +31,9 @@ cloud.google.com/go v0.92.2/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+Y
|
|||
cloud.google.com/go v0.92.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
|
||||
cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
|
||||
cloud.google.com/go v0.94.0/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
|
||||
cloud.google.com/go v0.94.1 h1:DwuSvDZ1pTYGbXo8yOJevCTr3BoBlE+OVkHAKiYQUXc=
|
||||
cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
|
||||
cloud.google.com/go v0.95.0 h1:JVWssQIj9cLwHmLjqWLptFa83o7HgqUictM6eyvGWJE=
|
||||
cloud.google.com/go v0.95.0/go.mod h1:MzZUAH870Y7E+c14j23Ir66FC1+PK8WLG7OG4SjP+0k=
|
||||
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.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
|
@ -43,6 +44,7 @@ cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7
|
|||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||
cloud.google.com/go/firestore v1.5.0/go.mod h1:c4nNYR1qdq7eaZ+jSc5fonrQN2k3M7sWATcYTiakjEo=
|
||||
cloud.google.com/go/firestore v1.6.0/go.mod h1:afJwI0vaXwAG54kI7A//lP/lSPDkQORQuMkv56TxEPU=
|
||||
cloud.google.com/go/kms v0.1.0 h1:VXAb5OzejDcyhFzIDeZ5n5AUdlsFnCyexuascIwWMj0=
|
||||
cloud.google.com/go/kms v0.1.0/go.mod h1:8Qp8PCAypHg4FdmlyW1QRAv09BGQ9Uzh7JnmIZxPk+c=
|
||||
cloud.google.com/go/monitoring v0.1.0/go.mod h1:Hpm3XfzJv+UTiXzCG5Ffp0wijzHTC7Cv4eR7o3x/fEE=
|
||||
|
@ -134,8 +136,8 @@ github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZo
|
|||
github.com/aws/aws-sdk-go v1.37.0/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.34/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
|
||||
github.com/aws/aws-sdk-go v1.40.45 h1:QN1nsY27ssD/JmW4s83qmSb+uL6DG4GmCDzjmJB4xUI=
|
||||
github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
|
||||
github.com/aws/aws-sdk-go v1.40.49 h1:kIbJYc4FZA2r4yxNU5giIR4HHLRkG9roFReWAsk0ZVQ=
|
||||
github.com/aws/aws-sdk-go v1.40.49/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.9.0/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.7.0/go.mod h1:w9+nMZ7soXCe5nT46Ri354SNhXDQ6v+V5wqDjnZE+GY=
|
||||
|
@ -180,6 +182,7 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX
|
|||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.1.1 h1:3XzfSMuUT0wBe1a3o5C0eOTcArhmmFAg2Jzh/7hhKqo=
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.1.1/go.mod h1:7NtUnP6eK+l6k483WSYNrq3Kb23bWV10IRV1TyeSpwM=
|
||||
|
@ -198,9 +201,7 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0/go.mod h1:J70FGZSbzsjecRTiTzER+3f1KZLNaXkuv+yeFTKoxM8=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0 h1:Fe5DW39aaoS/fqZiYlylEqQWIKznnbatWSHpWdFA3oQ=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
||||
|
@ -213,8 +214,8 @@ github.com/drakkan/crypto v0.0.0-20210918082254-e7eb8487714b h1:MZY6RAQFVhJous68
|
|||
github.com/drakkan/crypto v0.0.0-20210918082254-e7eb8487714b/go.mod h1:0hNoheD1tVu/m8WMkw/chBXf5VpwzL5fHQU25k79NKo=
|
||||
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 h1:LPH1dEblAOO/LoG7yHPMtBLXhQmjaga91/DDjWk9jWA=
|
||||
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU=
|
||||
github.com/drakkan/net v0.0.0-20210918081947-af83c6eab079 h1:tUP5m4c14gFrz/N4P6Z6q/59viuytG1A0q7BWh3VPFo=
|
||||
github.com/drakkan/net v0.0.0-20210918081947-af83c6eab079/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
github.com/drakkan/net v0.0.0-20210925100637-0aaabc6c2a0b h1:jIHHXQ4e5CGytFZPkDuCOM7YPL04z6jvnG/Iqm94PAY=
|
||||
github.com/drakkan/net v0.0.0-20210925100637-0aaabc6c2a0b/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/go.mod h1:kltMsfRMTHSFdMbK66XdS8mfMW77+FZA1fGY1xYMF84=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
|
@ -229,11 +230,12 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y
|
|||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc=
|
||||
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fclairamb/ftpserverlib v0.16.0 h1:uATwg14csmYcYaf3n7G16FCzuqYYI28PjBL4Jsk+yXo=
|
||||
github.com/fclairamb/ftpserverlib v0.16.0/go.mod h1:+Doq95UijHTIaJcWREhyu9dyQOqyoULbVU3OXgs8wEI=
|
||||
github.com/fclairamb/go-log v0.1.0 h1:fNoqk8w62i4EDEuRzDgHdDVTqMYSyr3DS981R7F2x/Y=
|
||||
|
@ -250,11 +252,10 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME
|
|||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
|
||||
github.com/go-chi/chi/v5 v5.0.4 h1:5e494iHzsYBiyXQAHHuI4tyJS9M3V84OuX3ufIIGHFo=
|
||||
github.com/go-chi/chi/v5 v5.0.4/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/jwtauth/v5 v5.0.1 h1:eyJ6Yx5VphEfjkqpZ7+LJEWThzyIcF5aN2QVpgqSIu0=
|
||||
github.com/go-chi/jwtauth/v5 v5.0.1/go.mod h1:+JtcRYGZsnA4+ur1LFlb4Bei3O9WeUzoMfDZWfUJuoY=
|
||||
github.com/go-chi/jwtauth/v5 v5.0.2 h1:CSKtr+b6Jnfy5T27sMaiBPxaVE/bjnjS3ramFQ0526w=
|
||||
github.com/go-chi/jwtauth/v5 v5.0.2/go.mod h1:TeA7vmPe3uYThvHw8O8W13HOOpOd4MTgToxL41gZyjs=
|
||||
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
|
||||
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
|
@ -286,7 +287,6 @@ github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL
|
|||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/goccy/go-json v0.4.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.7.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.7.8 h1:CvMH7LotYymYuLGEohBM1lTZWX4g6jzWUUl2aLFuBoE=
|
||||
github.com/goccy/go-json v0.7.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
|
@ -400,8 +400,10 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf
|
|||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||
github.com/hashicorp/consul/api v1.8.1/go.mod h1:sDjTOq0yUyv5G4h+BqSea7Fn6BU+XbolEz1952UB+mk=
|
||||
github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
|
||||
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/consul/sdk v0.7.0/go.mod h1:fY08Y9z5SvJqevyZNy6WWPXiG3KwBPAvlcdx16zZ0fM=
|
||||
github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
|
@ -537,24 +539,19 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.7/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A=
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
|
||||
github.com/lestrrat-go/blackmagic v1.0.0 h1:XzdxDbuQTz0RZZEmdU7cnQxUtFUzgCSPq8RCz4BxIi4=
|
||||
github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
|
||||
github.com/lestrrat-go/codegen v1.0.0/go.mod h1:JhJw6OQAuPEfVKUCLItpaVLumDGWQznd1VaXrBk9TdM=
|
||||
github.com/lestrrat-go/codegen v1.0.1/go.mod h1:JhJw6OQAuPEfVKUCLItpaVLumDGWQznd1VaXrBk9TdM=
|
||||
github.com/lestrrat-go/httpcc v1.0.0 h1:FszVC6cKfDvBKcJv646+lkh4GydQg2Z29scgUfkOpYc=
|
||||
github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE=
|
||||
github.com/lestrrat-go/iter v1.0.1 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A=
|
||||
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
|
||||
github.com/lestrrat-go/jwx v1.1.6/go.mod h1:c+R8G7qsaFNmTzYjU98A+sMh8Bo/MJqO9GnpqR+X024=
|
||||
github.com/lestrrat-go/jwx v1.2.6 h1:XAgfuHaOB7fDZ/6WhVgl8K89af768dU+3Nx4DlTbLIk=
|
||||
github.com/lestrrat-go/jwx v1.2.6/go.mod h1:tJuGuAI3LC71IicTx82Mz1n3w9woAs2bYJZpkjJQ5aU=
|
||||
github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
|
||||
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/lestrrat-go/pdebug/v3 v3.0.1/go.mod h1:za+m+Ve24yCxTEhR59N7UlnJomWwCiIqbJRmKeiADU4=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
|
@ -572,8 +569,9 @@ github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcncea
|
|||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.10 h1:KWqbp83oZ6YOEgIbNW3BM1Jbe2tz4jgmWA9FOuAF8bw=
|
||||
github.com/mattn/go-colorable v0.1.10/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-ieproxy v0.0.1 h1:qiyop7gCflfhwCzGyeT0gro3sF9AIg9HU98JORTkqfI=
|
||||
github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
|
@ -705,6 +703,7 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
|
|||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYIR88KRMEuODE=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
|
||||
|
@ -735,8 +734,9 @@ github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmq
|
|||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44=
|
||||
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
|
||||
github.com/spf13/viper v1.9.0 h1:yR6EXjTp0y0cLN8OZg1CRZmOBdI88UcGkhgyJhu6nZk=
|
||||
github.com/spf13/viper v1.9.0/go.mod h1:+i6ajR7OX2XaiBkrcZJFK21htRk7eDeLg7+O6bhUPP4=
|
||||
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
|
||||
|
@ -950,8 +950,8 @@ golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678 h1:J27LZFQBFoihqXoegpscI10HpjZ7B5WQLLKL2FZXQKw=
|
||||
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210925032602-92d5a993a665 h1:QOQNt6vCjMpXE7JSK5VvAzJC1byuN3FgTNSBwf+CJgI=
|
||||
golang.org/x/sys v0.0.0-20210925032602-92d5a993a665/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/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
@ -1148,8 +1148,9 @@ google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEc
|
|||
google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
||||
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
||||
google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
||||
google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4 h1:ysnBoUyeL/H6RCvNRhWHjKoDEmguI+mPU+qHgK8qv/w=
|
||||
google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
||||
google.golang.org/genproto v0.0.0-20210921142501-181ce0d877f6/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0 h1:5Tbluzus3QxoAJx4IefGt1W0HQZW4nuMrVk684jI74Q=
|
||||
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
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.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
|
@ -1175,8 +1176,9 @@ google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ
|
|||
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.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 v1.41.0 h1:f+PlOh7QV4iIJkPrx5NQ7qaNGFQ3OTse67yaDHfju4E=
|
||||
google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=
|
||||
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-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
|
|
|
@ -2,6 +2,7 @@ package httpd
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
|
@ -191,7 +192,8 @@ func doStartUserQuotaScan(w http.ResponseWriter, r *http.Request, username strin
|
|||
return
|
||||
}
|
||||
if !common.QuotaScans.AddUserQuotaScan(user.Username) {
|
||||
sendAPIResponse(w, r, err, "Another scan is already in progress", http.StatusConflict)
|
||||
sendAPIResponse(w, r, err, fmt.Sprintf("Another scan is already in progress for user %#v", username),
|
||||
http.StatusConflict)
|
||||
return
|
||||
}
|
||||
go doUserQuotaScan(user) //nolint:errcheck
|
||||
|
@ -209,7 +211,8 @@ func doStartFolderQuotaScan(w http.ResponseWriter, r *http.Request, name string)
|
|||
return
|
||||
}
|
||||
if !common.QuotaScans.AddVFolderQuotaScan(folder.Name) {
|
||||
sendAPIResponse(w, r, err, "Another scan is already in progress", http.StatusConflict)
|
||||
sendAPIResponse(w, r, err, fmt.Sprintf("Another scan is already in progress for folder %#v", name),
|
||||
http.StatusConflict)
|
||||
return
|
||||
}
|
||||
go doFolderQuotaScan(folder) //nolint:errcheck
|
||||
|
|
44
httpd/api_retention.go
Normal file
44
httpd/api_retention.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package httpd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/common"
|
||||
"github.com/drakkan/sftpgo/v2/dataprovider"
|
||||
)
|
||||
|
||||
func getRetentionChecks(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
render.JSON(w, r, common.RetentionChecks.Get())
|
||||
}
|
||||
|
||||
func startRetentionCheck(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
username := getURLParam(r, "username")
|
||||
user, err := dataprovider.UserExists(username)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
var check common.RetentionCheck
|
||||
err = render.DecodeJSON(r.Body, &check.Folders)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := check.Validate(); err != nil {
|
||||
sendAPIResponse(w, r, err, "Invalid folders to check", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
c := common.RetentionChecks.Add(check, &user)
|
||||
if c == nil {
|
||||
sendAPIResponse(w, r, err, fmt.Sprintf("Another check is already in progress for user %#v", username),
|
||||
http.StatusConflict)
|
||||
return
|
||||
}
|
||||
go c.Start()
|
||||
sendAPIResponse(w, r, err, "Check started", http.StatusAccepted)
|
||||
}
|
|
@ -75,6 +75,8 @@ const (
|
|||
userTOTPSavePath = "/api/v2/user/totp/save"
|
||||
user2FARecoveryCodesPath = "/api/v2/user/2fa/recoverycodes"
|
||||
userManageAPIKeyPath = "/api/v2/user/apikeyauth"
|
||||
retentionBasePath = "/api/v2/retention/users"
|
||||
retentionChecksPath = "/api/v2/retention/users/checks"
|
||||
healthzPath = "/healthz"
|
||||
webRootPathDefault = "/"
|
||||
webBasePathDefault = "/web"
|
||||
|
|
|
@ -99,6 +99,7 @@ const (
|
|||
userTOTPSavePath = "/api/v2/user/totp/save"
|
||||
user2FARecoveryCodesPath = "/api/v2/user/2fa/recoverycodes"
|
||||
userManageAPIKeyPath = "/api/v2/user/apikeyauth"
|
||||
retentionBasePath = "/api/v2/retention/users"
|
||||
healthzPath = "/healthz"
|
||||
webBasePath = "/web"
|
||||
webBasePathAdmin = "/web/admin"
|
||||
|
@ -1542,6 +1543,84 @@ func TestUserType(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRetentionAPI(t *testing.T) {
|
||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
checks, _, err := httpdtest.GetRetentionChecks(http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, checks, 0)
|
||||
|
||||
localFilePath := filepath.Join(user.HomeDir, "testdir", "testfile")
|
||||
err = os.MkdirAll(filepath.Dir(localFilePath), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
err = os.WriteFile(localFilePath, []byte("test data"), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
|
||||
folderRetention := []common.FolderRetention{
|
||||
{
|
||||
Path: "/",
|
||||
Retention: 0,
|
||||
DeleteEmptyDirs: true,
|
||||
},
|
||||
}
|
||||
|
||||
_, err = httpdtest.StartRetentionCheck(altAdminUsername, folderRetention, http.StatusNotFound)
|
||||
assert.NoError(t, err)
|
||||
|
||||
resp, err := httpdtest.StartRetentionCheck(user.Username, folderRetention, http.StatusBadRequest)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(resp), "Invalid folders to check")
|
||||
|
||||
folderRetention[0].Retention = 24
|
||||
_, err = httpdtest.StartRetentionCheck(user.Username, folderRetention, http.StatusAccepted)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Eventually(t, func() bool {
|
||||
return len(common.RetentionChecks.Get()) == 0
|
||||
}, 1000*time.Millisecond, 50*time.Millisecond)
|
||||
|
||||
assert.FileExists(t, localFilePath)
|
||||
|
||||
err = os.Chtimes(localFilePath, time.Now().Add(-48*time.Hour), time.Now().Add(-48*time.Hour))
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = httpdtest.StartRetentionCheck(user.Username, folderRetention, http.StatusAccepted)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Eventually(t, func() bool {
|
||||
return len(common.RetentionChecks.Get()) == 0
|
||||
}, 1000*time.Millisecond, 50*time.Millisecond)
|
||||
|
||||
assert.NoFileExists(t, localFilePath)
|
||||
assert.NoDirExists(t, filepath.Dir(localFilePath))
|
||||
|
||||
check := common.RetentionCheck{
|
||||
Folders: folderRetention,
|
||||
}
|
||||
c := common.RetentionChecks.Add(check, &user)
|
||||
assert.NotNil(t, c)
|
||||
|
||||
_, err = httpdtest.StartRetentionCheck(user.Username, folderRetention, http.StatusConflict)
|
||||
assert.NoError(t, err)
|
||||
|
||||
c.Start()
|
||||
assert.Len(t, common.RetentionChecks.Get(), 0)
|
||||
|
||||
token, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
req, _ := http.NewRequest(http.MethodPost, retentionBasePath+"/"+user.Username+"/check",
|
||||
bytes.NewBuffer([]byte("invalid json")))
|
||||
setBearerForReq(req, token)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestAddUserInvalidVirtualFolders(t *testing.T) {
|
||||
u := getTestUser()
|
||||
folderName := "fname"
|
||||
|
@ -9470,6 +9549,11 @@ func TestWebAdminSetupMock(t *testing.T) {
|
|||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusFound, rr)
|
||||
assert.Equal(t, webAdminSetupPath, rr.Header().Get("Location"))
|
||||
req, err = http.NewRequest(http.MethodGet, webClientLoginPath, nil)
|
||||
assert.NoError(t, err)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusFound, rr)
|
||||
assert.Equal(t, webAdminSetupPath, rr.Header().Get("Location"))
|
||||
|
||||
csrfToken, err := getCSRFToken(httpBaseURL + webAdminSetupPath)
|
||||
assert.NoError(t, err)
|
||||
|
|
|
@ -11,6 +11,7 @@ tags:
|
|||
- name: folders
|
||||
- name: users
|
||||
- name: users API
|
||||
- name: data retention
|
||||
info:
|
||||
title: SFTPGo
|
||||
description: |
|
||||
|
@ -745,6 +746,76 @@ paths:
|
|||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/retention/users/checks:
|
||||
get:
|
||||
tags:
|
||||
- data retention
|
||||
summary: Get retention checks
|
||||
description: Returns the active retention checks
|
||||
operationId: get_users_retention_checks
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/RetentionCheck'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/retention/users/{username}/check:
|
||||
parameters:
|
||||
- name: username
|
||||
in: path
|
||||
description: the username
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
post:
|
||||
tags:
|
||||
- data retention
|
||||
summary: Start a retention check
|
||||
description: 'Starts a new retention check for the given user. If a retention check for this user is already active a 409 status code is returned'
|
||||
operationId: start_user_retention_check
|
||||
requestBody:
|
||||
required: true
|
||||
description: 'Defines virtual paths to check and their retention time in hours'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/FolderRetention'
|
||||
responses:
|
||||
'202':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
message: Check 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/scans:
|
||||
get:
|
||||
tags:
|
||||
|
@ -3084,6 +3155,7 @@ components:
|
|||
- manage_system
|
||||
- manage_defender
|
||||
- view_defender
|
||||
- retention_checks
|
||||
description: |
|
||||
Admin permissions:
|
||||
* `*` - all permissions are granted
|
||||
|
@ -3096,8 +3168,11 @@ components:
|
|||
* `view_status` - view the server status is allowed
|
||||
* `manage_admins` - manage other admins is allowed
|
||||
* `manage_apikeys` - manage API keys is allowed
|
||||
* `quota_scans` - view and start quota scans is allowed
|
||||
* `manage_system` - backups and restores are allowed
|
||||
* `manage_defender` - remove ip from the dynamic blocklist is allowed
|
||||
* `view_defender` - list the dynamic blocklist is allowed
|
||||
* `retention_checks` - view and start retention checks is allowed
|
||||
LoginMethods:
|
||||
type: string
|
||||
enum:
|
||||
|
@ -3224,7 +3299,7 @@ components:
|
|||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: 'exposed virtual path, if no other specific filter is defined, the filter apply for sub directories too. For example if filters are defined for the paths "/" and "/sub" then the filters for "/" are applied for any file outside the "/sub" directory'
|
||||
description: 'exposed virtual path, if no other specific filter is defined, the filter applies for sub directories too. For example if filters are defined for the paths "/" and "/sub" then the filters for "/" are applied for any file outside the "/sub" directory'
|
||||
allowed_patterns:
|
||||
type: array
|
||||
items:
|
||||
|
@ -3334,6 +3409,7 @@ components:
|
|||
- GCP
|
||||
- AWS
|
||||
- VaultTransit
|
||||
- AzureKeyVault
|
||||
- Redacted
|
||||
description: 'Set to "Plain" to add or update an existing secret, set to "Redacted" to preserve the existing value'
|
||||
payload:
|
||||
|
@ -3546,7 +3622,7 @@ components:
|
|||
description: list of usernames associated with this virtual folder
|
||||
filesystem:
|
||||
$ref: '#/components/schemas/FilesystemConfig'
|
||||
description: Defines the filesystem for the virtual folder and the used quota limits. The same folder can be shared among multiple users and each user can have different quota limits or a different virtual path.
|
||||
description: 'Defines the filesystem for the virtual folder and the used quota limits. The same folder can be shared among multiple users and each user can have different quota limits or a different virtual path.'
|
||||
VirtualFolder:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/BaseVirtualFolder'
|
||||
|
@ -3861,12 +3937,43 @@ components:
|
|||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Transfer'
|
||||
FolderRetention:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: 'exposed virtual directory path, if no other specific retention is defined, the retention applies for sub directories too. For example if retention is defined for the paths "/" and "/sub" then the retention for "/" is applied for any file outside the "/sub" directory'
|
||||
example: '/'
|
||||
retention:
|
||||
type: integer
|
||||
description: retention time in hours. All the files with a modification time older than the defined value will be deleted. 0 means exclude this path
|
||||
example: 24
|
||||
delete_empty_dirs:
|
||||
type: boolean
|
||||
description: if enabled, empty directories will be deleted
|
||||
ignore_user_permissions:
|
||||
type: boolean
|
||||
description: 'if enabled, files will be deleted even if the user does not have the delete permission. The default is "false" which means that files will be skipped if the user does not have permission to delete them. File patterns filters will always be silently ignored'
|
||||
RetentionCheck:
|
||||
type: object
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
description: username to which the retention check refers
|
||||
folders:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/FolderRetention'
|
||||
start_time:
|
||||
type: integer
|
||||
format: int64
|
||||
description: check start time as unix timestamp in milliseconds
|
||||
QuotaScan:
|
||||
type: object
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
description: username with an active scan
|
||||
description: username to which the quota scan refers
|
||||
start_time:
|
||||
type: integer
|
||||
format: int64
|
||||
|
@ -3876,7 +3983,7 @@ components:
|
|||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: folder name with an active scan
|
||||
description: folder name to which the quota scan refers
|
||||
start_time:
|
||||
type: integer
|
||||
format: int64
|
||||
|
|
|
@ -133,6 +133,10 @@ func (s *httpdServer) renderClientLoginPage(w http.ResponseWriter, error string)
|
|||
|
||||
func (s *httpdServer) handleClientWebLogin(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
|
||||
if !dataprovider.HasAdmin() {
|
||||
http.Redirect(w, r, webAdminSetupPath, http.StatusFound)
|
||||
return
|
||||
}
|
||||
s.renderClientLoginPage(w, "")
|
||||
}
|
||||
|
||||
|
@ -970,6 +974,9 @@ func (s *httpdServer) initializeRouter() {
|
|||
router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Put(adminPath+"/{username}", updateAdmin)
|
||||
router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Delete(adminPath+"/{username}", deleteAdmin)
|
||||
router.With(checkPerm(dataprovider.PermAdminManageAdmins)).Put(adminPath+"/{username}/2fa/disable", disableAdmin2FA)
|
||||
router.With(checkPerm(dataprovider.PermAdminRetentionChecks)).Get(retentionChecksPath, getRetentionChecks)
|
||||
router.With(checkPerm(dataprovider.PermAdminRetentionChecks)).Post(retentionBasePath+"/{username}/check",
|
||||
startRetentionCheck)
|
||||
router.With(forbidAPIKeyAuthentication, checkPerm(dataprovider.PermAdminManageAPIKeys)).
|
||||
Get(apiKeysPath, getAPIKeys)
|
||||
router.With(forbidAPIKeyAuthentication, checkPerm(dataprovider.PermAdminManageAPIKeys)).
|
||||
|
|
|
@ -45,6 +45,8 @@ const (
|
|||
adminPath = "/api/v2/admins"
|
||||
adminPwdPath = "/api/v2/admin/changepwd"
|
||||
apiKeysPath = "/api/v2/apikeys"
|
||||
retentionBasePath = "/api/v2/retention/users"
|
||||
retentionChecksPath = "/api/v2/retention/users/checks"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -527,7 +529,40 @@ func UpdateQuotaUsage(user dataprovider.User, mode string, expectedStatusCode in
|
|||
if err != nil {
|
||||
return body, err
|
||||
}
|
||||
resp, err := sendHTTPRequest(http.MethodPut, url.String(), bytes.NewBuffer(userAsJSON), "", getDefaultToken())
|
||||
resp, err := sendHTTPRequest(http.MethodPut, url.String(), bytes.NewBuffer(userAsJSON), "application/json",
|
||||
getDefaultToken())
|
||||
if err != nil {
|
||||
return body, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ = getResponseBody(resp)
|
||||
return body, checkResponse(resp.StatusCode, expectedStatusCode)
|
||||
}
|
||||
|
||||
// GetRetentionChecks returns the active retention checks
|
||||
func GetRetentionChecks(expectedStatusCode int) ([]common.ActiveRetentionChecks, []byte, error) {
|
||||
var checks []common.ActiveRetentionChecks
|
||||
var body []byte
|
||||
resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(retentionChecksPath), nil, "", getDefaultToken())
|
||||
if err != nil {
|
||||
return checks, body, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
err = checkResponse(resp.StatusCode, expectedStatusCode)
|
||||
if err == nil && expectedStatusCode == http.StatusOK {
|
||||
err = render.DecodeJSON(resp.Body, &checks)
|
||||
} else {
|
||||
body, _ = getResponseBody(resp)
|
||||
}
|
||||
return checks, body, err
|
||||
}
|
||||
|
||||
// StartRetentionCheck starts a new retention check
|
||||
func StartRetentionCheck(username string, retention []common.FolderRetention, expectedStatusCode int) ([]byte, error) {
|
||||
var body []byte
|
||||
asJSON, _ := json.Marshal(retention)
|
||||
resp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(retentionBasePath, username, "check"),
|
||||
bytes.NewBuffer(asJSON), "application/json", getDefaultToken())
|
||||
if err != nil {
|
||||
return body, err
|
||||
}
|
||||
|
|
|
@ -100,7 +100,7 @@ func TestCleanupPasscodes(t *testing.T) {
|
|||
assert.Eventually(t, func() bool {
|
||||
_, ok := usedPasscodes.Load("key")
|
||||
return !ok
|
||||
}, 300*time.Millisecond, 100*time.Millisecond)
|
||||
}, 1000*time.Millisecond, 100*time.Millisecond)
|
||||
stopCleanupTicker()
|
||||
}
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ func (d *DirectoryPermissions) HasPerm(perm string) bool {
|
|||
// and they are not aware about these restrictions so they are not allowed
|
||||
// inside paths with extensions filters
|
||||
type PatternsFilter struct {
|
||||
// Virtual path, if no other specific filter is defined, the filter apply for
|
||||
// Virtual path, if no other specific filter is defined, the filter applies for
|
||||
// sub directories too.
|
||||
// For example if filters are defined for the paths "/" and "/sub" then the
|
||||
// filters for "/" are applied for any file outside the "/sub" directory
|
||||
|
|
Loading…
Reference in a new issue