add event manager

auto backup removed from setting. You can now schedule backups with
the event manager

Fixes #762

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2022-07-11 08:17:36 +02:00
parent e46051299f
commit 1b8f94c08f
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
54 changed files with 10238 additions and 869 deletions

View file

@ -21,6 +21,7 @@ Several storage backends are supported: local filesystem, encrypted local filesy
- Chroot isolation for local accounts. Cloud-based accounts can be restricted to a certain base path. - 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 and modification time. - 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 and modification time.
- [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. - [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.
- The [Event Manager](./docs/eventmanager.md) allows to define custom workflows based on server events or schedules.
- [Web based administration interface](./docs/web-admin.md) to easily manage users, folders and connections. - [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, manage and share their files in the browser. - [Web client interface](./docs/web-client.md) so that end users can change their credentials, manage and share their files in the browser.
- Public key and password authentication. Multiple public keys per-user are supported. - Public key and password authentication. Multiple public keys per-user are supported.

View file

@ -34,7 +34,7 @@ If the SMTP configuration is correct you should receive this email.`,
logger.ErrorToConsole("unable to initialize SMTP configuration: %v", err) logger.ErrorToConsole("unable to initialize SMTP configuration: %v", err)
os.Exit(1) os.Exit(1)
} }
err = smtp.SendEmail(smtpTestRecipient, "SFTPGo - Testing Email Settings", "It appears your SFTPGo email is setup correctly!", err = smtp.SendEmail([]string{smtpTestRecipient}, "SFTPGo - Testing Email Settings", "It appears your SFTPGo email is setup correctly!",
smtp.EmailContentTypeTextPlain) smtp.EmailContentTypeTextPlain)
if err != nil { if err != nil {
logger.WarnToConsole("Error sending email: %v", err) logger.WarnToConsole("Error sending email: %v", err)

View file

@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os/exec" "os/exec"
"path"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
@ -87,7 +88,8 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua
) error { ) error {
hasNotifiersPlugin := plugin.Handler.HasNotifiers() hasNotifiersPlugin := plugin.Handler.HasNotifiers()
hasHook := util.Contains(Config.Actions.ExecuteOn, operation) hasHook := util.Contains(Config.Actions.ExecuteOn, operation)
if !hasHook && !hasNotifiersPlugin { hasRules := dataprovider.EventManager.HasFsRules()
if !hasHook && !hasNotifiersPlugin && !hasRules {
return nil return nil
} }
notification := newActionNotification(&conn.User, operation, filePath, virtualPath, target, virtualTarget, sshCmd, notification := newActionNotification(&conn.User, operation, filePath, virtualPath, target, virtualTarget, sshCmd,
@ -95,15 +97,34 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua
if hasNotifiersPlugin { if hasNotifiersPlugin {
plugin.Handler.NotifyFsEvent(notification) plugin.Handler.NotifyFsEvent(notification)
} }
var errRes error
if hasRules {
errRes = dataprovider.EventManager.HandleFsEvent(dataprovider.EventParams{
Name: notification.Username,
Event: notification.Action,
Status: notification.Status,
VirtualPath: notification.VirtualPath,
FsPath: notification.Path,
VirtualTargetPath: notification.VirtualTargetPath,
FsTargetPath: notification.TargetPath,
ObjectName: path.Base(notification.VirtualPath),
FileSize: notification.FileSize,
Protocol: notification.Protocol,
IP: notification.IP,
Timestamp: notification.Timestamp,
Object: nil,
})
}
if hasHook { if hasHook {
if util.Contains(Config.Actions.ExecuteSync, operation) { if util.Contains(Config.Actions.ExecuteSync, operation) {
return actionHandler.Handle(notification) if errHook := actionHandler.Handle(notification); errHook != nil {
errRes = errHook
} }
} else {
go actionHandler.Handle(notification) //nolint:errcheck go actionHandler.Handle(notification) //nolint:errcheck
} }
return nil }
return errRes
} }
// ActionHandler handles a notification for a Protocol Action. // ActionHandler handles a notification for a Protocol Action.
@ -119,7 +140,6 @@ func newActionNotification(
err error, err error,
) *notifier.FsEvent { ) *notifier.FsEvent {
var bucket, endpoint string var bucket, endpoint string
status := 1
fsConfig := user.GetFsConfigForPath(virtualPath) fsConfig := user.GetFsConfigForPath(virtualPath)
@ -140,12 +160,6 @@ func newActionNotification(
endpoint = fsConfig.HTTPConfig.Endpoint endpoint = fsConfig.HTTPConfig.Endpoint
} }
if err == ErrQuotaExceeded {
status = 3
} else if err != nil {
status = 2
}
return &notifier.FsEvent{ return &notifier.FsEvent{
Action: operation, Action: operation,
Username: user.Username, Username: user.Username,
@ -158,7 +172,7 @@ func newActionNotification(
FsProvider: int(fsConfig.Provider), FsProvider: int(fsConfig.Provider),
Bucket: bucket, Bucket: bucket,
Endpoint: endpoint, Endpoint: endpoint,
Status: status, Status: getNotificationStatus(err),
Protocol: protocol, Protocol: protocol,
IP: ip, IP: ip,
SessionID: sessionID, SessionID: sessionID,
@ -211,7 +225,7 @@ func (h *defaultActionHandler) handleHTTP(event *notifier.FsEvent) error {
} }
} }
logger.Debug(event.Protocol, "", "notified operation %#v to URL: %v status code: %v, elapsed: %v err: %v", logger.Debug(event.Protocol, "", "notified operation %q to URL: %s status code: %d, elapsed: %s err: %v",
event.Action, u.Redacted(), respCode, time.Since(startTime), err) event.Action, u.Redacted(), respCode, time.Since(startTime), err)
return err return err
@ -243,22 +257,32 @@ func (h *defaultActionHandler) handleCommand(event *notifier.FsEvent) error {
func notificationAsEnvVars(event *notifier.FsEvent) []string { func notificationAsEnvVars(event *notifier.FsEvent) []string {
return []string{ return []string{
fmt.Sprintf("SFTPGO_ACTION=%v", event.Action), fmt.Sprintf("SFTPGO_ACTION=%s", event.Action),
fmt.Sprintf("SFTPGO_ACTION_USERNAME=%v", event.Username), fmt.Sprintf("SFTPGO_ACTION_USERNAME=%s", event.Username),
fmt.Sprintf("SFTPGO_ACTION_PATH=%v", event.Path), fmt.Sprintf("SFTPGO_ACTION_PATH=%s", event.Path),
fmt.Sprintf("SFTPGO_ACTION_TARGET=%v", event.TargetPath), fmt.Sprintf("SFTPGO_ACTION_TARGET=%s", event.TargetPath),
fmt.Sprintf("SFTPGO_ACTION_VIRTUAL_PATH=%v", event.VirtualPath), fmt.Sprintf("SFTPGO_ACTION_VIRTUAL_PATH=%s", event.VirtualPath),
fmt.Sprintf("SFTPGO_ACTION_VIRTUAL_TARGET=%v", event.VirtualTargetPath), fmt.Sprintf("SFTPGO_ACTION_VIRTUAL_TARGET=%s", event.VirtualTargetPath),
fmt.Sprintf("SFTPGO_ACTION_SSH_CMD=%v", event.SSHCmd), fmt.Sprintf("SFTPGO_ACTION_SSH_CMD=%s", event.SSHCmd),
fmt.Sprintf("SFTPGO_ACTION_FILE_SIZE=%v", event.FileSize), fmt.Sprintf("SFTPGO_ACTION_FILE_SIZE=%d", event.FileSize),
fmt.Sprintf("SFTPGO_ACTION_FS_PROVIDER=%v", event.FsProvider), fmt.Sprintf("SFTPGO_ACTION_FS_PROVIDER=%d", event.FsProvider),
fmt.Sprintf("SFTPGO_ACTION_BUCKET=%v", event.Bucket), fmt.Sprintf("SFTPGO_ACTION_BUCKET=%s", event.Bucket),
fmt.Sprintf("SFTPGO_ACTION_ENDPOINT=%v", event.Endpoint), fmt.Sprintf("SFTPGO_ACTION_ENDPOINT=%s", event.Endpoint),
fmt.Sprintf("SFTPGO_ACTION_STATUS=%v", event.Status), fmt.Sprintf("SFTPGO_ACTION_STATUS=%d", event.Status),
fmt.Sprintf("SFTPGO_ACTION_PROTOCOL=%v", event.Protocol), fmt.Sprintf("SFTPGO_ACTION_PROTOCOL=%s", event.Protocol),
fmt.Sprintf("SFTPGO_ACTION_IP=%v", event.IP), fmt.Sprintf("SFTPGO_ACTION_IP=%s", event.IP),
fmt.Sprintf("SFTPGO_ACTION_SESSION_ID=%v", event.SessionID), fmt.Sprintf("SFTPGO_ACTION_SESSION_ID=%s", event.SessionID),
fmt.Sprintf("SFTPGO_ACTION_OPEN_FLAGS=%v", event.OpenFlags), fmt.Sprintf("SFTPGO_ACTION_OPEN_FLAGS=%d", event.OpenFlags),
fmt.Sprintf("SFTPGO_ACTION_TIMESTAMP=%v", event.Timestamp), fmt.Sprintf("SFTPGO_ACTION_TIMESTAMP=%d", event.Timestamp),
} }
} }
func getNotificationStatus(err error) int {
status := 1
if err == ErrQuotaExceeded {
status = 3
} else if err != nil {
status = 2
}
return status
}

View file

@ -125,8 +125,6 @@ var (
Config Configuration Config Configuration
// Connections is the list of active connections // Connections is the list of active connections
Connections ActiveConnections Connections ActiveConnections
// QuotaScans is the list of active quota scans
QuotaScans ActiveScans
transfersChecker TransfersChecker transfersChecker TransfersChecker
periodicTimeoutTicker *time.Ticker periodicTimeoutTicker *time.Ticker
periodicTimeoutTickerDone chan bool periodicTimeoutTickerDone chan bool
@ -396,11 +394,11 @@ func (t *ConnectionTransfer) getConnectionTransferAsString() string {
case operationDownload: case operationDownload:
result += "DL " result += "DL "
} }
result += fmt.Sprintf("%#v ", t.VirtualPath) result += fmt.Sprintf("%q ", t.VirtualPath)
if t.Size > 0 { if t.Size > 0 {
elapsed := time.Since(util.GetTimeFromMsecSinceEpoch(t.StartTime)) elapsed := time.Since(util.GetTimeFromMsecSinceEpoch(t.StartTime))
speed := float64(t.Size) / float64(util.GetTimeAsMsSinceEpoch(time.Now())-t.StartTime) speed := float64(t.Size) / float64(util.GetTimeAsMsSinceEpoch(time.Now())-t.StartTime)
result += fmt.Sprintf("Size: %#v Elapsed: %#v Speed: \"%.1f KB/s\"", util.ByteCountIEC(t.Size), result += fmt.Sprintf("Size: %s Elapsed: %s Speed: \"%.1f KB/s\"", util.ByteCountIEC(t.Size),
util.GetDurationAsString(elapsed), speed) util.GetDurationAsString(elapsed), speed)
} }
return result return result
@ -1150,117 +1148,3 @@ func (c *ConnectionStatus) GetTransfersAsString() string {
} }
return result return result
} }
// ActiveQuotaScan defines an active quota scan for a user home dir
type ActiveQuotaScan struct {
// Username to which the quota scan refers
Username string `json:"username"`
// quota scan start time as unix timestamp in milliseconds
StartTime int64 `json:"start_time"`
}
// ActiveVirtualFolderQuotaScan defines an active quota scan for a virtual folder
type ActiveVirtualFolderQuotaScan struct {
// folder name to which the quota scan refers
Name string `json:"name"`
// quota scan start time as unix timestamp in milliseconds
StartTime int64 `json:"start_time"`
}
// ActiveScans holds the active quota scans
type ActiveScans struct {
sync.RWMutex
UserScans []ActiveQuotaScan
FolderScans []ActiveVirtualFolderQuotaScan
}
// GetUsersQuotaScans returns the active quota scans for users home directories
func (s *ActiveScans) GetUsersQuotaScans() []ActiveQuotaScan {
s.RLock()
defer s.RUnlock()
scans := make([]ActiveQuotaScan, len(s.UserScans))
copy(scans, s.UserScans)
return scans
}
// AddUserQuotaScan adds a user to the ones with active quota scans.
// Returns false if the user has a quota scan already running
func (s *ActiveScans) AddUserQuotaScan(username string) bool {
s.Lock()
defer s.Unlock()
for _, scan := range s.UserScans {
if scan.Username == username {
return false
}
}
s.UserScans = append(s.UserScans, ActiveQuotaScan{
Username: username,
StartTime: util.GetTimeAsMsSinceEpoch(time.Now()),
})
return true
}
// RemoveUserQuotaScan removes a user from the ones with active quota scans.
// Returns false if the user has no active quota scans
func (s *ActiveScans) RemoveUserQuotaScan(username string) bool {
s.Lock()
defer s.Unlock()
for idx, scan := range s.UserScans {
if scan.Username == username {
lastIdx := len(s.UserScans) - 1
s.UserScans[idx] = s.UserScans[lastIdx]
s.UserScans = s.UserScans[:lastIdx]
return true
}
}
return false
}
// GetVFoldersQuotaScans returns the active quota scans for virtual folders
func (s *ActiveScans) GetVFoldersQuotaScans() []ActiveVirtualFolderQuotaScan {
s.RLock()
defer s.RUnlock()
scans := make([]ActiveVirtualFolderQuotaScan, len(s.FolderScans))
copy(scans, s.FolderScans)
return scans
}
// AddVFolderQuotaScan adds a virtual folder to the ones with active quota scans.
// Returns false if the folder has a quota scan already running
func (s *ActiveScans) AddVFolderQuotaScan(folderName string) bool {
s.Lock()
defer s.Unlock()
for _, scan := range s.FolderScans {
if scan.Name == folderName {
return false
}
}
s.FolderScans = append(s.FolderScans, ActiveVirtualFolderQuotaScan{
Name: folderName,
StartTime: util.GetTimeAsMsSinceEpoch(time.Now()),
})
return true
}
// RemoveVFolderQuotaScan removes a folder from the ones with active quota scans.
// Returns false if the folder has no active quota scans
func (s *ActiveScans) RemoveVFolderQuotaScan(folderName string) bool {
s.Lock()
defer s.Unlock()
for idx, scan := range s.FolderScans {
if scan.Name == folderName {
lastIdx := len(s.FolderScans) - 1
s.FolderScans[idx] = s.FolderScans[lastIdx]
s.FolderScans = s.FolderScans[:lastIdx]
return true
}
}
return false
}

View file

@ -7,6 +7,7 @@ import (
"net" "net"
"os" "os"
"os/exec" "os/exec"
"path"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
@ -701,35 +702,6 @@ func TestConnectionStatus(t *testing.T) {
assert.Len(t, stats, 0) assert.Len(t, stats, 0)
} }
func TestQuotaScans(t *testing.T) {
username := "username"
assert.True(t, QuotaScans.AddUserQuotaScan(username))
assert.False(t, QuotaScans.AddUserQuotaScan(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))
assert.False(t, QuotaScans.AddVFolderQuotaScan(folderName))
if assert.Len(t, QuotaScans.GetVFoldersQuotaScans(), 1) {
assert.Equal(t, QuotaScans.GetVFoldersQuotaScans()[0].Name, folderName)
}
assert.True(t, QuotaScans.RemoveVFolderQuotaScan(folderName))
assert.False(t, QuotaScans.RemoveVFolderQuotaScan(folderName))
assert.Len(t, QuotaScans.GetVFoldersQuotaScans(), 0)
}
func TestProxyProtocolVersion(t *testing.T) { func TestProxyProtocolVersion(t *testing.T) {
c := Configuration{ c := Configuration{
ProxyProtocol: 0, ProxyProtocol: 0,
@ -1044,6 +1016,110 @@ func TestUserRecentActivity(t *testing.T) {
assert.True(t, res) assert.True(t, res)
} }
func TestEventRuleMatch(t *testing.T) {
conditions := dataprovider.EventConditions{
ProviderEvents: []string{"add", "update"},
Options: dataprovider.ConditionOptions{
Names: []dataprovider.ConditionPattern{
{
Pattern: "user1",
InverseMatch: true,
},
},
},
}
res := conditions.ProviderEventMatch(dataprovider.EventParams{
Name: "user1",
Event: "add",
})
assert.False(t, res)
res = conditions.ProviderEventMatch(dataprovider.EventParams{
Name: "user2",
Event: "update",
})
assert.True(t, res)
res = conditions.ProviderEventMatch(dataprovider.EventParams{
Name: "user2",
Event: "delete",
})
assert.False(t, res)
conditions.Options.ProviderObjects = []string{"api_key"}
res = conditions.ProviderEventMatch(dataprovider.EventParams{
Name: "user2",
Event: "update",
ObjectType: "share",
})
assert.False(t, res)
res = conditions.ProviderEventMatch(dataprovider.EventParams{
Name: "user2",
Event: "update",
ObjectType: "api_key",
})
assert.True(t, res)
// now test fs events
conditions = dataprovider.EventConditions{
FsEvents: []string{operationUpload, operationDownload},
Options: dataprovider.ConditionOptions{
Names: []dataprovider.ConditionPattern{
{
Pattern: "user*",
},
{
Pattern: "tester*",
},
},
FsPaths: []dataprovider.ConditionPattern{
{
Pattern: "*.txt",
},
},
Protocols: []string{ProtocolSFTP},
MinFileSize: 10,
MaxFileSize: 30,
},
}
params := dataprovider.EventParams{
Name: "tester4",
Event: operationDelete,
VirtualPath: "/path.txt",
Protocol: ProtocolSFTP,
ObjectName: "path.txt",
FileSize: 20,
}
res = conditions.FsEventMatch(params)
assert.False(t, res)
params.Event = operationDownload
res = conditions.FsEventMatch(params)
assert.True(t, res)
params.Name = "name"
res = conditions.FsEventMatch(params)
assert.False(t, res)
params.Name = "user5"
res = conditions.FsEventMatch(params)
assert.True(t, res)
params.VirtualPath = "/sub/f.jpg"
params.ObjectName = path.Base(params.VirtualPath)
res = conditions.FsEventMatch(params)
assert.False(t, res)
params.VirtualPath = "/sub/f.txt"
params.ObjectName = path.Base(params.VirtualPath)
res = conditions.FsEventMatch(params)
assert.True(t, res)
params.Protocol = ProtocolHTTP
res = conditions.FsEventMatch(params)
assert.False(t, res)
params.Protocol = ProtocolSFTP
params.FileSize = 5
res = conditions.FsEventMatch(params)
assert.False(t, res)
params.FileSize = 50
res = conditions.FsEventMatch(params)
assert.False(t, res)
params.FileSize = 25
res = conditions.FsEventMatch(params)
assert.True(t, res)
}
func BenchmarkBcryptHashing(b *testing.B) { func BenchmarkBcryptHashing(b *testing.B) {
bcryptPassword := "bcryptpassword" bcryptPassword := "bcryptpassword"
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {

View file

@ -35,11 +35,11 @@ const (
) )
var ( var (
// RetentionChecks is the list of active quota scans // RetentionChecks is the list of active retention checks
RetentionChecks ActiveRetentionChecks RetentionChecks ActiveRetentionChecks
) )
// ActiveRetentionChecks holds the active quota scans // ActiveRetentionChecks holds the active retention checks
type ActiveRetentionChecks struct { type ActiveRetentionChecks struct {
sync.RWMutex sync.RWMutex
Checks []RetentionCheck Checks []RetentionCheck
@ -390,7 +390,7 @@ func (c *RetentionCheck) sendEmailNotification(elapsed time.Duration, errCheck e
} }
startTime := time.Now() startTime := time.Now()
subject := fmt.Sprintf("Retention check completed for user %#v", c.conn.User.Username) subject := fmt.Sprintf("Retention check completed for user %#v", c.conn.User.Username)
if err := smtp.SendEmail(c.Email, subject, body.String(), smtp.EmailContentTypeTextHTML); err != nil { if err := smtp.SendEmail([]string{c.Email}, subject, body.String(), smtp.EmailContentTypeTextHTML); err != nil {
c.conn.Log(logger.LevelError, "unable to notify retention check result via email: %v, elapsed: %v", err, c.conn.Log(logger.LevelError, "unable to notify retention check result via email: %v, elapsed: %v", err,
time.Since(startTime)) time.Since(startTime))
return err return err

View file

@ -14,6 +14,7 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"sync"
"testing" "testing"
"time" "time"
@ -39,6 +40,7 @@ import (
"github.com/drakkan/sftpgo/v2/kms" "github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/mfa" "github.com/drakkan/sftpgo/v2/mfa"
"github.com/drakkan/sftpgo/v2/smtp"
"github.com/drakkan/sftpgo/v2/util" "github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
) )
@ -62,6 +64,7 @@ var (
homeBasePath string homeBasePath string
logFilePath string logFilePath string
testFileContent = []byte("test data") testFileContent = []byte("test data")
lastReceivedEmail receivedEmail
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
@ -172,6 +175,7 @@ func TestMain(m *testing.M) {
go func() { go func() {
if err := smtpd.ListenAndServe(smtpServerAddr, func(remoteAddr net.Addr, from string, to []string, data []byte) error { if err := smtpd.ListenAndServe(smtpServerAddr, func(remoteAddr net.Addr, from string, to []string, data []byte) error {
lastReceivedEmail.set(from, to, data)
return nil return nil
}, "SFTPGo test", "localhost"); err != nil { }, "SFTPGo test", "localhost"); err != nil {
logger.ErrorToConsole("could not start SMTP server: %v", err) logger.ErrorToConsole("could not start SMTP server: %v", err)
@ -2799,6 +2803,279 @@ func TestPasswordCaching(t *testing.T) {
assert.False(t, match) assert.False(t, match)
} }
func TestEventRule(t *testing.T) {
if runtime.GOOS == osWindows {
t.Skip("this test is not available on Windows")
}
smtpCfg := smtp.Config{
Host: "127.0.0.1",
Port: 2525,
From: "notification@example.com",
TemplatesPath: "templates",
}
err := smtpCfg.Initialize("..")
require.NoError(t, err)
a1 := dataprovider.BaseEventAction{
Name: "action1",
Type: dataprovider.ActionTypeHTTP,
Options: dataprovider.BaseEventActionOptions{
HTTPConfig: dataprovider.EventActionHTTPConfig{
Endpoint: "http://localhost",
Timeout: 20,
Method: http.MethodGet,
},
},
}
a2 := dataprovider.BaseEventAction{
Name: "action2",
Type: dataprovider.ActionTypeEmail,
Options: dataprovider.BaseEventActionOptions{
EmailConfig: dataprovider.EventActionEmailConfig{
Recipients: []string{"test1@example.com", "test2@example.com"},
Subject: `New "{{Event}}" from "{{Name}}"`,
Body: "Fs path {{FsPath}}, size: {{FileSize}}, protocol: {{Protocol}}, IP: {{IP}}",
},
},
}
a3 := dataprovider.BaseEventAction{
Name: "action3",
Type: dataprovider.ActionTypeEmail,
Options: dataprovider.BaseEventActionOptions{
EmailConfig: dataprovider.EventActionEmailConfig{
Recipients: []string{"failure@example.com"},
Subject: `Failed "{{Event}}" from "{{Name}}"`,
Body: "Fs path {{FsPath}}, protocol: {{Protocol}}, IP: {{IP}}",
},
},
}
action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)
assert.NoError(t, err)
action2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated)
assert.NoError(t, err)
action3, _, err := httpdtest.AddEventAction(a3, http.StatusCreated)
assert.NoError(t, err)
r1 := dataprovider.EventRule{
Name: "test rule1",
Trigger: dataprovider.EventTriggerFsEvent,
Conditions: dataprovider.EventConditions{
FsEvents: []string{"upload"},
Options: dataprovider.ConditionOptions{
FsPaths: []dataprovider.ConditionPattern{
{
Pattern: "/subdir/*.dat",
},
{
Pattern: "*.txt",
},
},
},
},
Actions: []dataprovider.EventAction{
{
BaseEventAction: dataprovider.BaseEventAction{
Name: action1.Name,
},
Order: 1,
Options: dataprovider.EventActionOptions{
ExecuteSync: true,
StopOnFailure: true,
},
},
{
BaseEventAction: dataprovider.BaseEventAction{
Name: action2.Name,
},
Order: 2,
},
{
BaseEventAction: dataprovider.BaseEventAction{
Name: action3.Name,
},
Order: 3,
Options: dataprovider.EventActionOptions{
IsFailureAction: true,
},
},
},
}
rule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)
assert.NoError(t, err)
r2 := dataprovider.EventRule{
Name: "test rule2",
Trigger: dataprovider.EventTriggerFsEvent,
Conditions: dataprovider.EventConditions{
FsEvents: []string{"download"},
Options: dataprovider.ConditionOptions{
FsPaths: []dataprovider.ConditionPattern{
{
Pattern: "*.dat",
},
},
},
},
Actions: []dataprovider.EventAction{
{
BaseEventAction: dataprovider.BaseEventAction{
Name: action2.Name,
},
Order: 1,
},
{
BaseEventAction: dataprovider.BaseEventAction{
Name: action3.Name,
},
Order: 2,
Options: dataprovider.EventActionOptions{
IsFailureAction: true,
},
},
},
}
rule2, _, err := httpdtest.AddEventRule(r2, http.StatusCreated)
assert.NoError(t, err)
r3 := dataprovider.EventRule{
Name: "test rule3",
Trigger: dataprovider.EventTriggerProviderEvent,
Conditions: dataprovider.EventConditions{
ProviderEvents: []string{"delete"},
},
Actions: []dataprovider.EventAction{
{
BaseEventAction: dataprovider.BaseEventAction{
Name: action2.Name,
},
Order: 1,
},
},
}
rule3, _, err := httpdtest.AddEventRule(r3, http.StatusCreated)
assert.NoError(t, err)
uploadScriptPath := filepath.Join(os.TempDir(), "upload.sh")
u := getTestUser()
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
movedFileName := "moved.dat"
movedPath := filepath.Join(user.HomeDir, movedFileName)
err = os.WriteFile(uploadScriptPath, getUploadScriptContent(movedPath, 0), 0755)
assert.NoError(t, err)
action1.Type = dataprovider.ActionTypeCommand
action1.Options = dataprovider.BaseEventActionOptions{
CmdConfig: dataprovider.EventActionCommandConfig{
Cmd: uploadScriptPath,
Timeout: 10,
EnvVars: []dataprovider.KeyValue{
{
Key: "SFTPGO_ACTION_PATH",
Value: "{{FsPath}}",
},
},
},
}
action1, _, err = httpdtest.UpdateEventAction(action1, http.StatusOK)
assert.NoError(t, err)
conn, client, err := getSftpClient(user)
if assert.NoError(t, err) {
defer conn.Close()
defer client.Close()
size := int64(32768)
// rule conditions does not match
err = writeSFTPFileNoCheck(testFileName, size, client)
assert.NoError(t, err)
info, err := client.Stat(testFileName)
if assert.NoError(t, err) {
assert.Equal(t, size, info.Size())
}
dirName := "subdir"
err = client.Mkdir(dirName)
assert.NoError(t, err)
// rule conditions match
lastReceivedEmail.reset()
err = writeSFTPFileNoCheck(path.Join(dirName, testFileName), size, client)
assert.NoError(t, err)
_, err = client.Stat(path.Join(dirName, testFileName))
assert.Error(t, err)
info, err = client.Stat(movedFileName)
if assert.NoError(t, err) {
assert.Equal(t, size, info.Size())
}
assert.Eventually(t, func() bool {
return lastReceivedEmail.get().From != ""
}, 3000*time.Millisecond, 100*time.Millisecond)
email := lastReceivedEmail.get()
assert.Len(t, email.To, 2)
assert.True(t, util.Contains(email.To, "test1@example.com"))
assert.True(t, util.Contains(email.To, "test2@example.com"))
assert.Contains(t, string(email.Data), fmt.Sprintf(`Subject: New "upload" from "%s"`, user.Username))
// remove the upload script to test the failure action
err = os.Remove(uploadScriptPath)
assert.NoError(t, err)
lastReceivedEmail.reset()
err = writeSFTPFileNoCheck(path.Join(dirName, testFileName), size, client)
assert.Error(t, err)
assert.Eventually(t, func() bool {
return lastReceivedEmail.get().From != ""
}, 3000*time.Millisecond, 100*time.Millisecond)
email = lastReceivedEmail.get()
assert.Len(t, email.To, 1)
assert.True(t, util.Contains(email.To, "failure@example.com"))
assert.Contains(t, string(email.Data), fmt.Sprintf(`Subject: Failed "upload" from "%s"`, user.Username))
// now test the download rule
lastReceivedEmail.reset()
f, err := client.Open(movedFileName)
assert.NoError(t, err)
contents, err := io.ReadAll(f)
assert.NoError(t, err)
err = f.Close()
assert.NoError(t, err)
assert.Len(t, contents, int(size))
assert.Eventually(t, func() bool {
return lastReceivedEmail.get().From != ""
}, 3000*time.Millisecond, 100*time.Millisecond)
email = lastReceivedEmail.get()
assert.Len(t, email.To, 2)
assert.True(t, util.Contains(email.To, "test1@example.com"))
assert.True(t, util.Contains(email.To, "test2@example.com"))
assert.Contains(t, string(email.Data), fmt.Sprintf(`Subject: New "download" from "%s"`, user.Username))
}
_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveEventRule(rule2, http.StatusOK)
assert.NoError(t, err)
assert.Eventually(t, func() bool {
return lastReceivedEmail.get().From != ""
}, 3000*time.Millisecond, 100*time.Millisecond)
email := lastReceivedEmail.get()
assert.Len(t, email.To, 2)
assert.True(t, util.Contains(email.To, "test1@example.com"))
assert.True(t, util.Contains(email.To, "test2@example.com"))
assert.Contains(t, string(email.Data), `Subject: New "delete" from "admin"`)
_, err = httpdtest.RemoveEventRule(rule3, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveEventAction(action2, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveEventAction(action3, http.StatusOK)
assert.NoError(t, err)
lastReceivedEmail.reset()
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
smtpCfg = smtp.Config{}
err = smtpCfg.Initialize("..")
require.NoError(t, err)
}
func TestSyncUploadAction(t *testing.T) { func TestSyncUploadAction(t *testing.T) {
if runtime.GOOS == osWindows { if runtime.GOOS == osWindows {
t.Skip("this test is not available on Windows") t.Skip("this test is not available on Windows")
@ -3865,3 +4142,39 @@ func printLatestLogs(maxNumberOfLines int) {
logger.DebugToConsole(line) logger.DebugToConsole(line)
} }
} }
type receivedEmail struct {
sync.RWMutex
From string
To []string
Data []byte
}
func (e *receivedEmail) set(from string, to []string, data []byte) {
e.Lock()
defer e.Unlock()
e.From = from
e.To = to
e.Data = data
}
func (e *receivedEmail) reset() {
e.Lock()
defer e.Unlock()
e.From = ""
e.To = nil
e.Data = nil
}
func (e *receivedEmail) get() receivedEmail {
e.RLock()
defer e.RUnlock()
return receivedEmail{
From: e.From,
To: e.To,
Data: e.Data,
}
}

View file

@ -342,11 +342,6 @@ func Init() {
NamingRules: 1, NamingRules: 1,
IsShared: 0, IsShared: 0,
BackupsPath: "backups", BackupsPath: "backups",
AutoBackup: dataprovider.AutoBackup{
Enabled: true,
Hour: "0",
DayOfWeek: "*",
},
}, },
HTTPDConfig: httpd.Conf{ HTTPDConfig: httpd.Conf{
Bindings: []httpd.Binding{defaultHTTPDBinding}, Bindings: []httpd.Binding{defaultHTTPDBinding},
@ -1919,9 +1914,6 @@ func setViperDefaults() {
viper.SetDefault("data_provider.naming_rules", globalConf.ProviderConf.NamingRules) viper.SetDefault("data_provider.naming_rules", globalConf.ProviderConf.NamingRules)
viper.SetDefault("data_provider.is_shared", globalConf.ProviderConf.IsShared) viper.SetDefault("data_provider.is_shared", globalConf.ProviderConf.IsShared)
viper.SetDefault("data_provider.backups_path", globalConf.ProviderConf.BackupsPath) viper.SetDefault("data_provider.backups_path", globalConf.ProviderConf.BackupsPath)
viper.SetDefault("data_provider.auto_backup.enabled", globalConf.ProviderConf.AutoBackup.Enabled)
viper.SetDefault("data_provider.auto_backup.hour", globalConf.ProviderConf.AutoBackup.Hour)
viper.SetDefault("data_provider.auto_backup.day_of_week", globalConf.ProviderConf.AutoBackup.DayOfWeek)
viper.SetDefault("httpd.templates_path", globalConf.HTTPDConfig.TemplatesPath) viper.SetDefault("httpd.templates_path", globalConf.HTTPDConfig.TemplatesPath)
viper.SetDefault("httpd.static_files_path", globalConf.HTTPDConfig.StaticFilesPath) viper.SetDefault("httpd.static_files_path", globalConf.HTTPDConfig.StaticFilesPath)
viper.SetDefault("httpd.openapi_path", globalConf.HTTPDConfig.OpenAPIPath) viper.SetDefault("httpd.openapi_path", globalConf.HTTPDConfig.OpenAPIPath)

View file

@ -33,6 +33,8 @@ const (
actionObjectAdmin = "admin" actionObjectAdmin = "admin"
actionObjectAPIKey = "api_key" actionObjectAPIKey = "api_key"
actionObjectShare = "share" actionObjectShare = "share"
actionObjectEventAction = "event_action"
actionObjectEventRule = "event_rule"
) )
func executeAction(operation, executor, ip, objectType, objectName string, object plugin.Renderer) { func executeAction(operation, executor, ip, objectType, objectName string, object plugin.Renderer) {
@ -46,6 +48,18 @@ func executeAction(operation, executor, ip, objectType, objectName string, objec
Timestamp: time.Now().UnixNano(), Timestamp: time.Now().UnixNano(),
}, object) }, object)
} }
if EventManager.hasProviderEvents() {
EventManager.handleProviderEvent(EventParams{
Name: executor,
ObjectName: objectName,
Event: operation,
Status: 1,
ObjectType: objectType,
IP: ip,
Timestamp: time.Now().UnixNano(),
Object: object,
})
}
if config.Actions.Hook == "" { if config.Actions.Hook == "" {
return return
} }
@ -74,7 +88,7 @@ func executeAction(operation, executor, ip, objectType, objectName string, objec
q.Add("ip", ip) q.Add("ip", ip)
q.Add("object_type", objectType) q.Add("object_type", objectType)
q.Add("object_name", objectName) q.Add("object_name", objectName)
q.Add("timestamp", fmt.Sprintf("%v", time.Now().UnixNano())) q.Add("timestamp", fmt.Sprintf("%d", time.Now().UnixNano()))
url.RawQuery = q.Encode() url.RawQuery = q.Encode()
startTime := time.Now() startTime := time.Now()
resp, err := httpclient.RetryablePost(url.String(), "application/json", bytes.NewBuffer(dataAsJSON)) resp, err := httpclient.RetryablePost(url.String(), "application/json", bytes.NewBuffer(dataAsJSON))
@ -104,13 +118,13 @@ func executeNotificationCommand(operation, executor, ip, objectType, objectName
cmd := exec.CommandContext(ctx, config.Actions.Hook) cmd := exec.CommandContext(ctx, config.Actions.Hook)
cmd.Env = append(env, cmd.Env = append(env,
fmt.Sprintf("SFTPGO_PROVIDER_ACTION=%v", operation), fmt.Sprintf("SFTPGO_PROVIDER_ACTION=%vs", operation),
fmt.Sprintf("SFTPGO_PROVIDER_OBJECT_TYPE=%v", objectType), fmt.Sprintf("SFTPGO_PROVIDER_OBJECT_TYPE=%s", objectType),
fmt.Sprintf("SFTPGO_PROVIDER_OBJECT_NAME=%v", objectName), fmt.Sprintf("SFTPGO_PROVIDER_OBJECT_NAME=%s", objectName),
fmt.Sprintf("SFTPGO_PROVIDER_USERNAME=%v", executor), fmt.Sprintf("SFTPGO_PROVIDER_USERNAME=%s", executor),
fmt.Sprintf("SFTPGO_PROVIDER_IP=%v", ip), fmt.Sprintf("SFTPGO_PROVIDER_IP=%s", ip),
fmt.Sprintf("SFTPGO_PROVIDER_TIMESTAMP=%v", util.GetTimeAsMsSinceEpoch(time.Now())), fmt.Sprintf("SFTPGO_PROVIDER_TIMESTAMP=%d", util.GetTimeAsMsSinceEpoch(time.Now())),
fmt.Sprintf("SFTPGO_PROVIDER_OBJECT=%v", string(objectAsJSON))) fmt.Sprintf("SFTPGO_PROVIDER_OBJECT=%s", string(objectAsJSON)))
startTime := time.Now() startTime := time.Now()
err := cmd.Run() err := cmd.Run()

View file

@ -40,6 +40,7 @@ const (
PermAdminRetentionChecks = "retention_checks" PermAdminRetentionChecks = "retention_checks"
PermAdminMetadataChecks = "metadata_checks" PermAdminMetadataChecks = "metadata_checks"
PermAdminViewEvents = "view_events" PermAdminViewEvents = "view_events"
PermAdminManageEventRules = "manage_event_rules"
) )
var ( var (

View file

@ -9,6 +9,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"path/filepath" "path/filepath"
"sort"
"time" "time"
bolt "go.etcd.io/bbolt" bolt "go.etcd.io/bbolt"
@ -20,7 +21,7 @@ import (
) )
const ( const (
boltDatabaseVersion = 19 boltDatabaseVersion = 20
) )
var ( var (
@ -30,10 +31,12 @@ var (
adminsBucket = []byte("admins") adminsBucket = []byte("admins")
apiKeysBucket = []byte("api_keys") apiKeysBucket = []byte("api_keys")
sharesBucket = []byte("shares") sharesBucket = []byte("shares")
actionsBucket = []byte("events_actions")
rulesBucket = []byte("events_rules")
dbVersionBucket = []byte("db_version") dbVersionBucket = []byte("db_version")
dbVersionKey = []byte("version") dbVersionKey = []byte("version")
boltBuckets = [][]byte{usersBucket, groupsBucket, foldersBucket, adminsBucket, apiKeysBucket, boltBuckets = [][]byte{usersBucket, groupsBucket, foldersBucket, adminsBucket, apiKeysBucket,
sharesBucket, dbVersionBucket} sharesBucket, actionsBucket, rulesBucket, dbVersionBucket}
) )
// BoltProvider defines the auth provider for bolt key/value store // BoltProvider defines the auth provider for bolt key/value store
@ -629,30 +632,35 @@ func (p *BoltProvider) deleteUser(user User) error {
if err != nil { if err != nil {
return err return err
} }
exists := bucket.Get([]byte(user.Username)) var u []byte
if exists == nil { if u = bucket.Get([]byte(user.Username)); u == nil {
return util.NewRecordNotFoundError(fmt.Sprintf("user %#v does not exist", user.Username)) return util.NewRecordNotFoundError(fmt.Sprintf("username %q does not exist", user.Username))
}
var oldUser User
err = json.Unmarshal(u, &oldUser)
if err != nil {
return err
} }
if len(user.VirtualFolders) > 0 { if len(oldUser.VirtualFolders) > 0 {
foldersBucket, err := p.getFoldersBucket(tx) foldersBucket, err := p.getFoldersBucket(tx)
if err != nil { if err != nil {
return err return err
} }
for idx := range user.VirtualFolders { for idx := range oldUser.VirtualFolders {
err = p.removeRelationFromFolderMapping(user.VirtualFolders[idx], user.Username, "", foldersBucket) err = p.removeRelationFromFolderMapping(oldUser.VirtualFolders[idx], oldUser.Username, "", foldersBucket)
if err != nil { if err != nil {
return err return err
} }
} }
} }
if len(user.Groups) > 0 { if len(oldUser.Groups) > 0 {
groupBucket, err := p.getGroupsBucket(tx) groupBucket, err := p.getGroupsBucket(tx)
if err != nil { if err != nil {
return err return err
} }
for idx := range user.Groups { for idx := range oldUser.Groups {
err = p.removeUserFromGroupMapping(user.Username, user.Groups[idx].Name, groupBucket) err = p.removeUserFromGroupMapping(oldUser.Username, oldUser.Groups[idx].Name, groupBucket)
if err != nil { if err != nil {
return err return err
} }
@ -1362,6 +1370,7 @@ func (p *BoltProvider) updateGroup(group *Group) error {
} }
group.ID = oldGroup.ID group.ID = oldGroup.ID
group.CreatedAt = oldGroup.CreatedAt group.CreatedAt = oldGroup.CreatedAt
group.Users = oldGroup.Users
group.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now()) group.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
buf, err := json.Marshal(group) buf, err := json.Marshal(group)
if err != nil { if err != nil {
@ -1932,6 +1941,490 @@ func (p *BoltProvider) cleanupSharedSessions(sessionType SessionType, before int
return ErrNotImplemented return ErrNotImplemented
} }
func (p *BoltProvider) getEventActions(limit, offset int, order string, minimal bool) ([]BaseEventAction, error) {
if limit <= 0 {
return nil, nil
}
actions := make([]BaseEventAction, 0, limit)
err := p.dbHandle.View(func(tx *bolt.Tx) error {
bucket, err := p.getActionsBucket(tx)
if err != nil {
return err
}
itNum := 0
cursor := bucket.Cursor()
if order == OrderASC {
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
itNum++
if itNum <= offset {
continue
}
var action BaseEventAction
err = json.Unmarshal(v, &action)
if err != nil {
return err
}
action.PrepareForRendering()
actions = append(actions, action)
if len(actions) >= limit {
break
}
}
} else {
for k, v := cursor.Last(); k != nil; k, v = cursor.Prev() {
itNum++
if itNum <= offset {
continue
}
var action BaseEventAction
err = json.Unmarshal(v, &action)
if err != nil {
return err
}
action.PrepareForRendering()
actions = append(actions, action)
if len(actions) >= limit {
break
}
}
}
return nil
})
return actions, err
}
func (p *BoltProvider) dumpEventActions() ([]BaseEventAction, error) {
actions := make([]BaseEventAction, 0, 50)
err := p.dbHandle.View(func(tx *bolt.Tx) error {
bucket, err := p.getActionsBucket(tx)
if err != nil {
return err
}
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var action BaseEventAction
err = json.Unmarshal(v, &action)
if err != nil {
return err
}
actions = append(actions, action)
}
return nil
})
return actions, err
}
func (p *BoltProvider) eventActionExists(name string) (BaseEventAction, error) {
var action BaseEventAction
err := p.dbHandle.View(func(tx *bolt.Tx) error {
bucket, err := p.getActionsBucket(tx)
if err != nil {
return err
}
k := bucket.Get([]byte(name))
if k == nil {
return util.NewRecordNotFoundError(fmt.Sprintf("action %q does not exist", name))
}
return json.Unmarshal(k, &action)
})
return action, err
}
func (p *BoltProvider) addEventAction(action *BaseEventAction) error {
err := action.validate()
if err != nil {
return err
}
return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket, err := p.getActionsBucket(tx)
if err != nil {
return err
}
if a := bucket.Get([]byte(action.Name)); a != nil {
return fmt.Errorf("event action %s already exists", action.Name)
}
id, err := bucket.NextSequence()
if err != nil {
return err
}
action.ID = int64(id)
action.Rules = nil
buf, err := json.Marshal(action)
if err != nil {
return err
}
return bucket.Put([]byte(action.Name), buf)
})
}
func (p *BoltProvider) updateEventAction(action *BaseEventAction) error {
err := action.validate()
if err != nil {
return err
}
return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket, err := p.getActionsBucket(tx)
if err != nil {
return err
}
var a []byte
if a = bucket.Get([]byte(action.Name)); a == nil {
return util.NewRecordNotFoundError(fmt.Sprintf("event action %s does not exist", action.Name))
}
var oldAction BaseEventAction
err = json.Unmarshal(a, &oldAction)
if err != nil {
return err
}
action.ID = oldAction.ID
action.Name = oldAction.Name
action.Rules = nil
if len(oldAction.Rules) > 0 {
rulesBucket, err := p.getRulesBucket(tx)
if err != nil {
return err
}
var relatedRules []string
for _, ruleName := range oldAction.Rules {
r := rulesBucket.Get([]byte(ruleName))
if r != nil {
relatedRules = append(relatedRules, ruleName)
var rule EventRule
err := json.Unmarshal(r, &rule)
if err != nil {
return err
}
rule.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
buf, err := json.Marshal(rule)
if err != nil {
return err
}
if err = rulesBucket.Put([]byte(rule.Name), buf); err != nil {
return err
}
setLastRuleUpdate()
}
}
action.Rules = relatedRules
}
buf, err := json.Marshal(action)
if err != nil {
return err
}
return bucket.Put([]byte(action.Name), buf)
})
}
func (p *BoltProvider) deleteEventAction(action BaseEventAction) error {
return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket, err := p.getActionsBucket(tx)
if err != nil {
return err
}
var a []byte
if a = bucket.Get([]byte(action.Name)); a == nil {
return util.NewRecordNotFoundError(fmt.Sprintf("action %s does not exist", action.Name))
}
var oldAction BaseEventAction
err = json.Unmarshal(a, &oldAction)
if err != nil {
return err
}
if len(oldAction.Rules) > 0 {
return util.NewValidationError(fmt.Sprintf("action %s is referenced, it cannot be removed", oldAction.Name))
}
return bucket.Delete([]byte(action.Name))
})
}
func (p *BoltProvider) getEventRules(limit, offset int, order string) ([]EventRule, error) {
if limit <= 0 {
return nil, nil
}
rules := make([]EventRule, 0, limit)
err := p.dbHandle.View(func(tx *bolt.Tx) error {
bucket, err := p.getRulesBucket(tx)
if err != nil {
return err
}
actionsBucket, err := p.getActionsBucket(tx)
if err != nil {
return err
}
itNum := 0
cursor := bucket.Cursor()
if order == OrderASC {
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
itNum++
if itNum <= offset {
continue
}
var rule EventRule
rule, err = p.joinRuleAndActions(v, actionsBucket)
if err != nil {
return err
}
rule.PrepareForRendering()
rules = append(rules, rule)
if len(rules) >= limit {
break
}
}
} else {
for k, v := cursor.Last(); k != nil; k, v = cursor.Prev() {
itNum++
if itNum <= offset {
continue
}
var rule EventRule
rule, err = p.joinRuleAndActions(v, actionsBucket)
if err != nil {
return err
}
rule.PrepareForRendering()
rules = append(rules, rule)
if len(rules) >= limit {
break
}
}
}
return err
})
return rules, err
}
func (p *BoltProvider) dumpEventRules() ([]EventRule, error) {
rules := make([]EventRule, 0, 50)
err := p.dbHandle.View(func(tx *bolt.Tx) error {
bucket, err := p.getRulesBucket(tx)
if err != nil {
return err
}
actionsBucket, err := p.getActionsBucket(tx)
if err != nil {
return err
}
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
rule, err := p.joinRuleAndActions(v, actionsBucket)
if err != nil {
return err
}
rules = append(rules, rule)
}
return nil
})
return rules, err
}
func (p *BoltProvider) getRecentlyUpdatedRules(after int64) ([]EventRule, error) {
if getLastRuleUpdate() < after {
return nil, nil
}
rules := make([]EventRule, 0, 10)
err := p.dbHandle.View(func(tx *bolt.Tx) error {
bucket, err := p.getRulesBucket(tx)
if err != nil {
return err
}
actionsBucket, err := p.getActionsBucket(tx)
if err != nil {
return err
}
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var rule EventRule
err := json.Unmarshal(v, &rule)
if err != nil {
return err
}
if rule.UpdatedAt < after {
continue
}
var actions []EventAction
for idx := range rule.Actions {
action := &rule.Actions[idx]
var baseAction BaseEventAction
k := actionsBucket.Get([]byte(action.Name))
if k == nil {
continue
}
err = json.Unmarshal(k, &baseAction)
if err != nil {
continue
}
baseAction.Options.SetEmptySecretsIfNil()
action.BaseEventAction = baseAction
actions = append(actions, *action)
}
rule.Actions = actions
rules = append(rules, rule)
}
return nil
})
return rules, err
}
func (p *BoltProvider) eventRuleExists(name string) (EventRule, error) {
var rule EventRule
err := p.dbHandle.View(func(tx *bolt.Tx) error {
bucket, err := p.getRulesBucket(tx)
if err != nil {
return err
}
r := bucket.Get([]byte(name))
if r == nil {
return util.NewRecordNotFoundError(fmt.Sprintf("event rule %q does not exist", name))
}
actionsBucket, err := p.getActionsBucket(tx)
if err != nil {
return err
}
rule, err = p.joinRuleAndActions(r, actionsBucket)
return err
})
return rule, err
}
func (p *BoltProvider) addEventRule(rule *EventRule) error {
if err := rule.validate(); err != nil {
return err
}
return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket, err := p.getRulesBucket(tx)
if err != nil {
return err
}
actionsBucket, err := p.getActionsBucket(tx)
if err != nil {
return err
}
if r := bucket.Get([]byte(rule.Name)); r != nil {
return fmt.Errorf("event rule %q already exists", rule.Name)
}
id, err := bucket.NextSequence()
if err != nil {
return err
}
rule.ID = int64(id)
rule.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
rule.UpdatedAt = rule.CreatedAt
for idx := range rule.Actions {
if err = p.addRuleToActionMapping(rule.Name, rule.Actions[idx].Name, actionsBucket); err != nil {
return err
}
}
sort.Slice(rule.Actions, func(i, j int) bool {
return rule.Actions[i].Order < rule.Actions[j].Order
})
buf, err := json.Marshal(rule)
if err != nil {
return err
}
err = bucket.Put([]byte(rule.Name), buf)
if err == nil {
setLastRuleUpdate()
}
return err
})
}
func (p *BoltProvider) updateEventRule(rule *EventRule) error {
if err := rule.validate(); err != nil {
return err
}
return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket, err := p.getRulesBucket(tx)
if err != nil {
return err
}
actionsBucket, err := p.getActionsBucket(tx)
if err != nil {
return err
}
var r []byte
if r = bucket.Get([]byte(rule.Name)); r == nil {
return util.NewRecordNotFoundError(fmt.Sprintf("event rule %q does not exist", rule.Name))
}
var oldRule EventRule
if err = json.Unmarshal(r, &oldRule); err != nil {
return err
}
for idx := range oldRule.Actions {
if err = p.removeRuleFromActionMapping(rule.Name, oldRule.Actions[idx].Name, actionsBucket); err != nil {
return err
}
}
for idx := range rule.Actions {
if err = p.addRuleToActionMapping(rule.Name, rule.Actions[idx].Name, actionsBucket); err != nil {
return err
}
}
rule.ID = oldRule.ID
rule.CreatedAt = oldRule.CreatedAt
rule.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
buf, err := json.Marshal(rule)
if err != nil {
return err
}
sort.Slice(rule.Actions, func(i, j int) bool {
return rule.Actions[i].Order < rule.Actions[j].Order
})
err = bucket.Put([]byte(rule.Name), buf)
if err == nil {
setLastRuleUpdate()
}
return err
})
}
func (p *BoltProvider) deleteEventRule(rule EventRule, softDelete bool) error {
return p.dbHandle.Update(func(tx *bolt.Tx) error {
bucket, err := p.getRulesBucket(tx)
if err != nil {
return err
}
var r []byte
if r = bucket.Get([]byte(rule.Name)); r == nil {
return util.NewRecordNotFoundError(fmt.Sprintf("event rule %q does not exist", rule.Name))
}
var oldRule EventRule
if err = json.Unmarshal(r, &oldRule); err != nil {
return err
}
if len(oldRule.Actions) > 0 {
actionsBucket, err := p.getActionsBucket(tx)
if err != nil {
return err
}
for idx := range oldRule.Actions {
if err = p.removeRuleFromActionMapping(rule.Name, oldRule.Actions[idx].Name, actionsBucket); err != nil {
return err
}
}
}
return bucket.Delete([]byte(rule.Name))
})
}
func (p *BoltProvider) getTaskByName(name string) (Task, error) {
return Task{}, ErrNotImplemented
}
func (p *BoltProvider) addTask(name string) error {
return ErrNotImplemented
}
func (p *BoltProvider) updateTask(name string, version int64) error {
return ErrNotImplemented
}
func (p *BoltProvider) updateTaskTimestamp(name string) error {
return ErrNotImplemented
}
func (p *BoltProvider) close() error { func (p *BoltProvider) close() error {
return p.dbHandle.Close() return p.dbHandle.Close()
} }
@ -1959,6 +2452,10 @@ func (p *BoltProvider) migrateDatabase() error {
providerLog(logger.LevelError, "%v", err) providerLog(logger.LevelError, "%v", err)
logger.ErrorToConsole("%v", err) logger.ErrorToConsole("%v", err)
return err return err
case version == 19:
logger.InfoToConsole(fmt.Sprintf("updating database version: %d -> 20", version))
providerLog(logger.LevelInfo, "updating database version: %d -> 20", version)
return updateBoltDatabaseVersion(p.dbHandle, 20)
default: default:
if version > boltDatabaseVersion { if version > boltDatabaseVersion {
providerLog(logger.LevelError, "database version %v is newer than the supported one: %v", version, providerLog(logger.LevelError, "database version %v is newer than the supported one: %v", version,
@ -1980,6 +2477,22 @@ func (p *BoltProvider) revertDatabase(targetVersion int) error {
return errors.New("current version match target version, nothing to do") return errors.New("current version match target version, nothing to do")
} }
switch dbVersion.Version { switch dbVersion.Version {
case 20:
logger.InfoToConsole("downgrading database version: 20 -> 19")
providerLog(logger.LevelInfo, "downgrading database version: 20 -> 19")
err := p.dbHandle.Update(func(tx *bolt.Tx) error {
for _, bucketName := range [][]byte{actionsBucket, rulesBucket} {
err := tx.DeleteBucket(bucketName)
if err != nil && !errors.Is(err, bolt.ErrBucketNotFound) {
return err
}
}
return nil
})
if err != nil {
return err
}
return updateBoltDatabaseVersion(p.dbHandle, 19)
default: default:
return fmt.Errorf("database version not handled: %v", dbVersion.Version) return fmt.Errorf("database version not handled: %v", dbVersion.Version)
} }
@ -1997,6 +2510,32 @@ func (p *BoltProvider) resetDatabase() error {
}) })
} }
func (p *BoltProvider) joinRuleAndActions(r []byte, actionsBucket *bolt.Bucket) (EventRule, error) {
var rule EventRule
err := json.Unmarshal(r, &rule)
if err != nil {
return rule, err
}
var actions []EventAction
for idx := range rule.Actions {
action := &rule.Actions[idx]
var baseAction BaseEventAction
k := actionsBucket.Get([]byte(action.Name))
if k == nil {
continue
}
err = json.Unmarshal(k, &baseAction)
if err != nil {
continue
}
baseAction.Options.SetEmptySecretsIfNil()
action.BaseEventAction = baseAction
actions = append(actions, *action)
}
rule.Actions = actions
return rule, nil
}
func (p *BoltProvider) joinGroupAndFolders(g []byte, foldersBucket *bolt.Bucket) (Group, error) { func (p *BoltProvider) joinGroupAndFolders(g []byte, foldersBucket *bolt.Bucket) (Group, error) {
var group Group var group Group
err := json.Unmarshal(g, &group) err := json.Unmarshal(g, &group)
@ -2078,10 +2617,59 @@ func (p *BoltProvider) addFolderInternal(folder vfs.BaseVirtualFolder, bucket *b
return bucket.Put([]byte(folder.Name), buf) return bucket.Put([]byte(folder.Name), buf)
} }
func (p *BoltProvider) addRuleToActionMapping(ruleName, actionName string, bucket *bolt.Bucket) error {
a := bucket.Get([]byte(actionName))
if a == nil {
return util.NewGenericError(fmt.Sprintf("action %q does not exist", actionName))
}
var action BaseEventAction
err := json.Unmarshal(a, &action)
if err != nil {
return err
}
if !util.Contains(action.Rules, ruleName) {
action.Rules = append(action.Rules, ruleName)
buf, err := json.Marshal(action)
if err != nil {
return err
}
return bucket.Put([]byte(action.Name), buf)
}
return nil
}
func (p *BoltProvider) removeRuleFromActionMapping(ruleName, actionName string, bucket *bolt.Bucket) error {
a := bucket.Get([]byte(actionName))
if a == nil {
providerLog(logger.LevelWarn, "action %q does not exist, cannot remove from mapping", actionName)
return nil
}
var action BaseEventAction
err := json.Unmarshal(a, &action)
if err != nil {
return err
}
if util.Contains(action.Rules, ruleName) {
var rules []string
for _, r := range action.Rules {
if r != ruleName {
rules = append(rules, r)
}
}
action.Rules = util.RemoveDuplicates(rules, false)
buf, err := json.Marshal(action)
if err != nil {
return err
}
return bucket.Put([]byte(action.Name), buf)
}
return nil
}
func (p *BoltProvider) addUserToGroupMapping(username, groupname string, bucket *bolt.Bucket) error { func (p *BoltProvider) addUserToGroupMapping(username, groupname string, bucket *bolt.Bucket) error {
g := bucket.Get([]byte(groupname)) g := bucket.Get([]byte(groupname))
if g == nil { if g == nil {
return util.NewRecordNotFoundError(fmt.Sprintf("group %#v does not exist", groupname)) return util.NewRecordNotFoundError(fmt.Sprintf("group %q does not exist", groupname))
} }
var group Group var group Group
err := json.Unmarshal(g, &group) err := json.Unmarshal(g, &group)
@ -2102,7 +2690,7 @@ func (p *BoltProvider) addUserToGroupMapping(username, groupname string, bucket
func (p *BoltProvider) removeUserFromGroupMapping(username, groupname string, bucket *bolt.Bucket) error { func (p *BoltProvider) removeUserFromGroupMapping(username, groupname string, bucket *bolt.Bucket) error {
g := bucket.Get([]byte(groupname)) g := bucket.Get([]byte(groupname))
if g == nil { if g == nil {
return util.NewRecordNotFoundError(fmt.Sprintf("group %#v does not exist", groupname)) return util.NewRecordNotFoundError(fmt.Sprintf("group %q does not exist", groupname))
} }
var group Group var group Group
err := json.Unmarshal(g, &group) err := json.Unmarshal(g, &group)
@ -2116,7 +2704,7 @@ func (p *BoltProvider) removeUserFromGroupMapping(username, groupname string, bu
users = append(users, u) users = append(users, u)
} }
} }
group.Users = users group.Users = util.RemoveDuplicates(users, false)
buf, err := json.Marshal(group) buf, err := json.Marshal(group)
if err != nil { if err != nil {
return err return err
@ -2372,7 +2960,7 @@ func (p *BoltProvider) getGroupsBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
var err error var err error
bucket := tx.Bucket(groupsBucket) bucket := tx.Bucket(groupsBucket)
if bucket == nil { if bucket == nil {
err = fmt.Errorf("unable to find groups buckets, bolt database structure not correcly defined") err = fmt.Errorf("unable to find groups bucket, bolt database structure not correcly defined")
} }
return bucket, err return bucket, err
} }
@ -2381,7 +2969,25 @@ func (p *BoltProvider) getFoldersBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
var err error var err error
bucket := tx.Bucket(foldersBucket) bucket := tx.Bucket(foldersBucket)
if bucket == nil { if bucket == nil {
err = fmt.Errorf("unable to find folders buckets, bolt database structure not correcly defined") err = fmt.Errorf("unable to find folders bucket, bolt database structure not correcly defined")
}
return bucket, err
}
func (p *BoltProvider) getActionsBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
var err error
bucket := tx.Bucket(actionsBucket)
if bucket == nil {
err = fmt.Errorf("unable to find event actions bucket, bolt database structure not correcly defined")
}
return bucket, err
}
func (p *BoltProvider) getRulesBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
var err error
bucket := tx.Bucket(rulesBucket)
if bucket == nil {
err = fmt.Errorf("unable to find event rules bucket, bolt database structure not correcly defined")
} }
return bucket, err return bucket, err
} }
@ -2405,7 +3011,7 @@ func getBoltDatabaseVersion(dbHandle *bolt.DB) (schemaVersion, error) {
return dbVersion, err return dbVersion, err
} }
/*func updateBoltDatabaseVersion(dbHandle *bolt.DB, version int) error { func updateBoltDatabaseVersion(dbHandle *bolt.DB, version int) error {
err := dbHandle.Update(func(tx *bolt.Tx) error { err := dbHandle.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket(dbVersionBucket) bucket := tx.Bucket(dbVersionBucket)
if bucket == nil { if bucket == nil {
@ -2421,4 +3027,4 @@ func getBoltDatabaseVersion(dbHandle *bolt.DB) (schemaVersion, error) {
return bucket.Put(dbVersionKey, buf) return bucket.Put(dbVersionKey, buf)
}) })
return err return err
}*/ }

View file

@ -72,7 +72,7 @@ const (
CockroachDataProviderName = "cockroachdb" CockroachDataProviderName = "cockroachdb"
// DumpVersion defines the version for the dump. // DumpVersion defines the version for the dump.
// For restore/load we support the current version and the previous one // For restore/load we support the current version and the previous one
DumpVersion = 12 DumpVersion = 13
argonPwdPrefix = "$argon2id$" argonPwdPrefix = "$argon2id$"
bcryptPwdPrefix = "$2a$" bcryptPwdPrefix = "$2a$"
@ -168,6 +168,10 @@ var (
sqlTableUsersGroupsMapping string sqlTableUsersGroupsMapping string
sqlTableGroupsFoldersMapping string sqlTableGroupsFoldersMapping string
sqlTableSharedSessions string sqlTableSharedSessions string
sqlTableEventsActions string
sqlTableEventsRules string
sqlTableRulesActionsMapping string
sqlTableTasks string
sqlTableSchemaVersion string sqlTableSchemaVersion string
argon2Params *argon2id.Params argon2Params *argon2id.Params
lastLoginMinDelay = 10 * time.Minute lastLoginMinDelay = 10 * time.Minute
@ -189,6 +193,10 @@ func initSQLTables() {
sqlTableUsersGroupsMapping = "users_groups_mapping" sqlTableUsersGroupsMapping = "users_groups_mapping"
sqlTableGroupsFoldersMapping = "groups_folders_mapping" sqlTableGroupsFoldersMapping = "groups_folders_mapping"
sqlTableSharedSessions = "shared_sessions" sqlTableSharedSessions = "shared_sessions"
sqlTableEventsActions = "events_actions"
sqlTableEventsRules = "events_rules"
sqlTableRulesActionsMapping = "rules_actions_mapping"
sqlTableTasks = "tasks"
sqlTableSchemaVersion = "schema_version" sqlTableSchemaVersion = "schema_version"
} }
@ -250,24 +258,6 @@ type ProviderStatus struct {
Error string `json:"error"` Error string `json:"error"`
} }
// AutoBackup defines the settings for automatic provider backups.
// Example: hour "0" and day_of_week "*" means a backup every day at midnight.
// The backup file name is in the format backup_<day_of_week>_<hour>.json
// files with the same name will be overwritten
type AutoBackup struct {
Enabled bool `json:"enabled" mapstructure:"enabled"`
// hour as standard cron expression. Allowed values: 0-23.
// Allowed special characters: asterisk (*), slash (/), comma (,), hyphen (-).
// More info about special characters here:
// https://pkg.go.dev/github.com/robfig/cron#hdr-Special_Characters
Hour string `json:"hour" mapstructure:"hour"`
// Day of the week as cron expression. Allowed values: 0-6 (Sunday to Saturday).
// Allowed special characters: asterisk (*), slash (/), comma (,), hyphen (-), question mark (?).
// More info about special characters here:
// https://pkg.go.dev/github.com/robfig/cron#hdr-Special_Characters
DayOfWeek string `json:"day_of_week" mapstructure:"day_of_week"`
}
// Config provider configuration // Config provider configuration
type Config struct { type Config struct {
// Driver name, must be one of the SupportedProviders // Driver name, must be one of the SupportedProviders
@ -417,8 +407,6 @@ type Config struct {
IsShared int `json:"is_shared" mapstructure:"is_shared"` IsShared int `json:"is_shared" mapstructure:"is_shared"`
// Path to the backup directory. This can be an absolute path or a path relative to the config dir // Path to the backup directory. This can be an absolute path or a path relative to the config dir
BackupsPath string `json:"backups_path" mapstructure:"backups_path"` BackupsPath string `json:"backups_path" mapstructure:"backups_path"`
// Settings for automatic backups
AutoBackup AutoBackup `json:"auto_backup" mapstructure:"auto_backup"`
} }
// GetShared returns the provider share mode // GetShared returns the provider share mode
@ -430,7 +418,7 @@ func (c *Config) GetShared() int {
} }
func (c *Config) convertName(name string) string { func (c *Config) convertName(name string) string {
if c.NamingRules == 0 { if c.NamingRules <= 1 {
return name return name
} }
if c.NamingRules&2 != 0 { if c.NamingRules&2 != 0 {
@ -464,31 +452,32 @@ func (c *Config) requireCustomTLSForMySQL() bool {
return false return false
} }
func (c *Config) doBackup() { func (c *Config) doBackup() error {
now := time.Now() now := time.Now().UTC()
outputFile := filepath.Join(c.BackupsPath, fmt.Sprintf("backup_%v_%v.json", now.Weekday(), now.Hour())) outputFile := filepath.Join(c.BackupsPath, fmt.Sprintf("backup_%s_%d.json", now.Weekday(), now.Hour()))
providerLog(logger.LevelDebug, "starting auto backup to file %#v", outputFile) eventManagerLog(logger.LevelDebug, "starting backup to file %q", outputFile)
err := os.MkdirAll(filepath.Dir(outputFile), 0700) err := os.MkdirAll(filepath.Dir(outputFile), 0700)
if err != nil { if err != nil {
providerLog(logger.LevelError, "unable to create backup dir %#v: %v", outputFile, err) eventManagerLog(logger.LevelError, "unable to create backup dir %q: %v", outputFile, err)
return return fmt.Errorf("unable to create backup dir: %w", err)
} }
backup, err := DumpData() backup, err := DumpData()
if err != nil { if err != nil {
providerLog(logger.LevelError, "unable to execute backup: %v", err) eventManagerLog(logger.LevelError, "unable to execute backup: %v", err)
return return fmt.Errorf("unable to dump backup data: %w", err)
} }
dump, err := json.Marshal(backup) dump, err := json.Marshal(backup)
if err != nil { if err != nil {
providerLog(logger.LevelError, "unable to marshal backup as JSON: %v", err) eventManagerLog(logger.LevelError, "unable to marshal backup as JSON: %v", err)
return return fmt.Errorf("unable to marshal backup data as JSON: %w", err)
} }
err = os.WriteFile(outputFile, dump, 0600) err = os.WriteFile(outputFile, dump, 0600)
if err != nil { if err != nil {
providerLog(logger.LevelError, "unable to save backup: %v", err) eventManagerLog(logger.LevelError, "unable to save backup: %v", err)
return return fmt.Errorf("unable to save backup: %w", err)
} }
providerLog(logger.LevelDebug, "auto backup saved to %#v", outputFile) eventManagerLog(logger.LevelDebug, "auto backup saved to %q", outputFile)
return nil
} }
// ConvertName converts the given name based on the configured rules // ConvertName converts the given name based on the configured rules
@ -592,6 +581,8 @@ type BackupData struct {
Admins []Admin `json:"admins"` Admins []Admin `json:"admins"`
APIKeys []APIKey `json:"api_keys"` APIKeys []APIKey `json:"api_keys"`
Shares []Share `json:"shares"` Shares []Share `json:"shares"`
EventActions []BaseEventAction `json:"event_actions"`
EventRules []EventRule `json:"event_rules"`
Version int `json:"version"` Version int `json:"version"`
} }
@ -704,6 +695,23 @@ type Provider interface {
deleteSharedSession(key string) error deleteSharedSession(key string) error
getSharedSession(key string) (Session, error) getSharedSession(key string) (Session, error)
cleanupSharedSessions(sessionType SessionType, before int64) error cleanupSharedSessions(sessionType SessionType, before int64) error
getEventActions(limit, offset int, order string, minimal bool) ([]BaseEventAction, error)
dumpEventActions() ([]BaseEventAction, error)
eventActionExists(name string) (BaseEventAction, error)
addEventAction(action *BaseEventAction) error
updateEventAction(action *BaseEventAction) error
deleteEventAction(action BaseEventAction) error
getEventRules(limit, offset int, order string) ([]EventRule, error)
dumpEventRules() ([]EventRule, error)
getRecentlyUpdatedRules(after int64) ([]EventRule, error)
eventRuleExists(name string) (EventRule, error)
addEventRule(rule *EventRule) error
updateEventRule(rule *EventRule) error
deleteEventRule(rule EventRule, softDelete bool) error
getTaskByName(name string) (Task, error)
addTask(name string) error
updateTask(name string, version int64) error
updateTaskTimestamp(name string) error
checkAvailability() error checkAvailability() error
close() error close() error
reloadConfig() error reloadConfig() error
@ -851,13 +859,19 @@ func validateSQLTablesPrefix() error {
sqlTableUsersGroupsMapping = config.SQLTablesPrefix + sqlTableUsersGroupsMapping sqlTableUsersGroupsMapping = config.SQLTablesPrefix + sqlTableUsersGroupsMapping
sqlTableGroupsFoldersMapping = config.SQLTablesPrefix + sqlTableGroupsFoldersMapping sqlTableGroupsFoldersMapping = config.SQLTablesPrefix + sqlTableGroupsFoldersMapping
sqlTableSharedSessions = config.SQLTablesPrefix + sqlTableSharedSessions sqlTableSharedSessions = config.SQLTablesPrefix + sqlTableSharedSessions
sqlTableEventsActions = config.SQLTablesPrefix + sqlTableEventsActions
sqlTableEventsRules = config.SQLTablesPrefix + sqlTableEventsRules
sqlTableRulesActionsMapping = config.SQLTablesPrefix + sqlTableRulesActionsMapping
sqlTableTasks = config.SQLTablesPrefix + sqlTableTasks
sqlTableSchemaVersion = config.SQLTablesPrefix + sqlTableSchemaVersion sqlTableSchemaVersion = config.SQLTablesPrefix + sqlTableSchemaVersion
providerLog(logger.LevelDebug, "sql table for users %q, folders %q users folders mapping %q admins %q "+ providerLog(logger.LevelDebug, "sql table for users %q, folders %q users folders mapping %q admins %q "+
"api keys %q shares %q defender hosts %q defender events %q transfers %q groups %q "+ "api keys %q shares %q defender hosts %q defender events %q transfers %q groups %q "+
"users groups mapping %q groups folders mapping %q shared sessions %q schema version %q", "users groups mapping %q groups folders mapping %q shared sessions %q schema version %q"+
"events actions %q events rules %q rules actions mapping %q tasks %q",
sqlTableUsers, sqlTableFolders, sqlTableUsersFoldersMapping, sqlTableAdmins, sqlTableAPIKeys, sqlTableUsers, sqlTableFolders, sqlTableUsersFoldersMapping, sqlTableAdmins, sqlTableAPIKeys,
sqlTableShares, sqlTableDefenderHosts, sqlTableDefenderEvents, sqlTableActiveTransfers, sqlTableGroups, sqlTableShares, sqlTableDefenderHosts, sqlTableDefenderEvents, sqlTableActiveTransfers, sqlTableGroups,
sqlTableUsersGroupsMapping, sqlTableGroupsFoldersMapping, sqlTableSharedSessions, sqlTableSchemaVersion) sqlTableUsersGroupsMapping, sqlTableGroupsFoldersMapping, sqlTableSharedSessions, sqlTableSchemaVersion,
sqlTableEventsActions, sqlTableEventsRules, sqlTableRulesActionsMapping, sqlTableTasks)
} }
return nil return nil
} }
@ -1468,6 +1482,102 @@ func APIKeyExists(keyID string) (APIKey, error) {
return provider.apiKeyExists(keyID) return provider.apiKeyExists(keyID)
} }
// GetEventActions returns an array of event actions respecting limit and offset
func GetEventActions(limit, offset int, order string, minimal bool) ([]BaseEventAction, error) {
return provider.getEventActions(limit, offset, order, minimal)
}
// EventActionExists returns the event action with the given name if it exists
func EventActionExists(name string) (BaseEventAction, error) {
name = config.convertName(name)
return provider.eventActionExists(name)
}
// AddEventAction adds a new event action
func AddEventAction(action *BaseEventAction, executor, ipAddress string) error {
action.Name = config.convertName(action.Name)
err := provider.addEventAction(action)
if err == nil {
executeAction(operationAdd, executor, ipAddress, actionObjectEventAction, action.Name, action)
}
return err
}
// UpdateEventAction updates an existing event action
func UpdateEventAction(action *BaseEventAction, executor, ipAddress string) error {
err := provider.updateEventAction(action)
if err == nil {
EventManager.loadRules()
executeAction(operationUpdate, executor, ipAddress, actionObjectEventAction, action.Name, action)
}
return err
}
// DeleteEventAction deletes an existing event action
func DeleteEventAction(name string, executor, ipAddress string) error {
name = config.convertName(name)
action, err := provider.eventActionExists(name)
if err != nil {
return err
}
if len(action.Rules) > 0 {
errorString := fmt.Sprintf("the event action %#q is referenced, it cannot be removed", action.Name)
return util.NewValidationError(errorString)
}
err = provider.deleteEventAction(action)
if err == nil {
executeAction(operationDelete, executor, ipAddress, actionObjectEventAction, action.Name, &action)
}
return err
}
// GetEventRules returns an array of event rules respecting limit and offset
func GetEventRules(limit, offset int, order string) ([]EventRule, error) {
return provider.getEventRules(limit, offset, order)
}
// EventRuleExists returns the event rule with the given name if it exists
func EventRuleExists(name string) (EventRule, error) {
name = config.convertName(name)
return provider.eventRuleExists(name)
}
// AddEventRule adds a new event rule
func AddEventRule(rule *EventRule, executor, ipAddress string) error {
rule.Name = config.convertName(rule.Name)
err := provider.addEventRule(rule)
if err == nil {
EventManager.loadRules()
executeAction(operationAdd, executor, ipAddress, actionObjectEventRule, rule.Name, rule)
}
return err
}
// UpdateEventRule updates an existing event rule
func UpdateEventRule(rule *EventRule, executor, ipAddress string) error {
err := provider.updateEventRule(rule)
if err == nil {
EventManager.loadRules()
executeAction(operationUpdate, executor, ipAddress, actionObjectEventRule, rule.Name, rule)
}
return err
}
// DeleteEventRule deletes an existing event rule
func DeleteEventRule(name string, executor, ipAddress string) error {
name = config.convertName(name)
rule, err := provider.eventRuleExists(name)
if err != nil {
return err
}
err = provider.deleteEventRule(rule, config.GetShared() == 1)
if err == nil {
EventManager.RemoveRule(rule.Name)
executeAction(operationDelete, executor, ipAddress, actionObjectEventRule, rule.Name, &rule)
}
return err
}
// HasAdmin returns true if the first admin has been created // HasAdmin returns true if the first admin has been created
// and so SFTPGo is ready to be used // and so SFTPGo is ready to be used
func HasAdmin() bool { func HasAdmin() bool {
@ -1794,7 +1904,7 @@ func GetFolders(limit, offset int, order string, minimal bool) ([]vfs.BaseVirtua
return provider.getFolders(limit, offset, order, minimal) return provider.getFolders(limit, offset, order, minimal)
} }
// DumpData returns all users, folders, admins, api keys, shares // DumpData returns all users, groups, folders, admins, api keys, shares, actions, rules
func DumpData() (BackupData, error) { func DumpData() (BackupData, error) {
var data BackupData var data BackupData
groups, err := provider.dumpGroups() groups, err := provider.dumpGroups()
@ -1821,12 +1931,22 @@ func DumpData() (BackupData, error) {
if err != nil { if err != nil {
return data, err return data, err
} }
actions, err := provider.dumpEventActions()
if err != nil {
return data, err
}
rules, err := provider.dumpEventRules()
if err != nil {
return data, err
}
data.Users = users data.Users = users
data.Groups = groups data.Groups = groups
data.Folders = folders data.Folders = folders
data.Admins = admins data.Admins = admins
data.APIKeys = apiKeys data.APIKeys = apiKeys
data.Shares = shares data.Shares = shares
data.EventActions = actions
data.EventRules = rules
data.Version = DumpVersion data.Version = DumpVersion
return data, err return data, err
} }

1096
dataprovider/eventrule.go Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,460 @@
package dataprovider
import (
"fmt"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/robfig/cron/v3"
"github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/plugin"
"github.com/drakkan/sftpgo/v2/util"
)
var (
// EventManager handle the supported event rules actions
EventManager EventRulesContainer
)
func init() {
EventManager = EventRulesContainer{
schedulesMapping: make(map[string][]cron.EntryID),
}
}
// EventRulesContainer stores event rules by trigger
type EventRulesContainer struct {
sync.RWMutex
FsEvents []EventRule
ProviderEvents []EventRule
Schedules []EventRule
schedulesMapping map[string][]cron.EntryID
lastLoad int64
}
func (r *EventRulesContainer) getLastLoadTime() int64 {
return atomic.LoadInt64(&r.lastLoad)
}
func (r *EventRulesContainer) setLastLoadTime(modTime int64) {
atomic.StoreInt64(&r.lastLoad, modTime)
}
// RemoveRule deletes the rule with the specified name
func (r *EventRulesContainer) RemoveRule(name string) {
r.Lock()
defer r.Unlock()
r.removeRuleInternal(name)
eventManagerLog(logger.LevelDebug, "event rules updated after delete, fs events: %d, provider events: %d, schedules: %d",
len(r.FsEvents), len(r.ProviderEvents), len(r.Schedules))
}
func (r *EventRulesContainer) removeRuleInternal(name string) {
for idx := range r.FsEvents {
if r.FsEvents[idx].Name == name {
lastIdx := len(r.FsEvents) - 1
r.FsEvents[idx] = r.FsEvents[lastIdx]
r.FsEvents = r.FsEvents[:lastIdx]
eventManagerLog(logger.LevelDebug, "removed rule %q from fs events", name)
return
}
}
for idx := range r.ProviderEvents {
if r.ProviderEvents[idx].Name == name {
lastIdx := len(r.ProviderEvents) - 1
r.ProviderEvents[idx] = r.ProviderEvents[lastIdx]
r.ProviderEvents = r.ProviderEvents[:lastIdx]
eventManagerLog(logger.LevelDebug, "removed rule %q from provider events", name)
return
}
}
for idx := range r.Schedules {
if r.Schedules[idx].Name == name {
if schedules, ok := r.schedulesMapping[name]; ok {
for _, entryID := range schedules {
eventManagerLog(logger.LevelDebug, "removing scheduled entry id %d for rule %q", entryID, name)
scheduler.Remove(entryID)
}
delete(r.schedulesMapping, name)
}
lastIdx := len(r.Schedules) - 1
r.Schedules[idx] = r.Schedules[lastIdx]
r.Schedules = r.Schedules[:lastIdx]
eventManagerLog(logger.LevelDebug, "removed rule %q from scheduled events", name)
return
}
}
}
func (r *EventRulesContainer) addUpdateRuleInternal(rule EventRule) {
r.removeRuleInternal(rule.Name)
if rule.DeletedAt > 0 {
deletedAt := util.GetTimeFromMsecSinceEpoch(rule.DeletedAt)
if deletedAt.Add(30 * time.Minute).Before(time.Now()) {
eventManagerLog(logger.LevelDebug, "removing rule %q deleted at %s", rule.Name, deletedAt)
go provider.deleteEventRule(rule, false) //nolint:errcheck
}
return
}
switch rule.Trigger {
case EventTriggerFsEvent:
r.FsEvents = append(r.FsEvents, rule)
eventManagerLog(logger.LevelDebug, "added rule %q to fs events", rule.Name)
case EventTriggerProviderEvent:
r.ProviderEvents = append(r.ProviderEvents, rule)
eventManagerLog(logger.LevelDebug, "added rule %q to provider events", rule.Name)
case EventTriggerSchedule:
r.Schedules = append(r.Schedules, rule)
eventManagerLog(logger.LevelDebug, "added rule %q to scheduled events", rule.Name)
for _, schedule := range rule.Conditions.Schedules {
cronSpec := schedule.getCronSpec()
job := &cronJob{
ruleName: ConvertName(rule.Name),
}
entryID, err := scheduler.AddJob(cronSpec, job)
if err != nil {
eventManagerLog(logger.LevelError, "unable to add scheduled rule %q: %v", rule.Name, err)
} else {
r.schedulesMapping[rule.Name] = append(r.schedulesMapping[rule.Name], entryID)
eventManagerLog(logger.LevelDebug, "scheduled rule %q added, id: %d, active scheduling rules: %d",
rule.Name, entryID, len(r.schedulesMapping))
}
}
default:
eventManagerLog(logger.LevelError, "unsupported trigger: %d", rule.Trigger)
}
}
func (r *EventRulesContainer) loadRules() {
eventManagerLog(logger.LevelDebug, "loading updated rules")
modTime := util.GetTimeAsMsSinceEpoch(time.Now())
rules, err := provider.getRecentlyUpdatedRules(r.getLastLoadTime())
if err != nil {
eventManagerLog(logger.LevelError, "unable to load event rules: %v", err)
return
}
eventManagerLog(logger.LevelDebug, "recently updated event rules loaded: %d", len(rules))
if len(rules) > 0 {
r.Lock()
defer r.Unlock()
for _, rule := range rules {
r.addUpdateRuleInternal(rule)
}
}
eventManagerLog(logger.LevelDebug, "event rules updated, fs events: %d, provider events: %d, schedules: %d",
len(r.FsEvents), len(r.ProviderEvents), len(r.Schedules))
r.setLastLoadTime(modTime)
}
// HasFsRules returns true if there are any rules for filesystem event triggers
func (r *EventRulesContainer) HasFsRules() bool {
r.RLock()
defer r.RUnlock()
return len(r.FsEvents) > 0
}
func (r *EventRulesContainer) hasProviderEvents() bool {
r.RLock()
defer r.RUnlock()
return len(r.ProviderEvents) > 0
}
// HandleFsEvent executes the rules actions defined for the specified event
func (r *EventRulesContainer) HandleFsEvent(params EventParams) error {
r.RLock()
var rulesWithSyncActions, rulesAsync []EventRule
for _, rule := range r.FsEvents {
if rule.Conditions.FsEventMatch(params) {
hasSyncActions := false
for _, action := range rule.Actions {
if action.Options.ExecuteSync {
hasSyncActions = true
break
}
}
if hasSyncActions {
rulesWithSyncActions = append(rulesWithSyncActions, rule)
} else {
rulesAsync = append(rulesAsync, rule)
}
}
}
r.RUnlock()
if len(rulesAsync) > 0 {
go executeAsyncActions(rulesAsync, params)
}
if len(rulesWithSyncActions) > 0 {
return executeSyncActions(rulesWithSyncActions, params)
}
return nil
}
func (r *EventRulesContainer) handleProviderEvent(params EventParams) {
r.RLock()
defer r.RUnlock()
var rules []EventRule
for _, rule := range r.ProviderEvents {
if rule.Conditions.ProviderEventMatch(params) {
rules = append(rules, rule)
}
}
go executeAsyncActions(rules, params)
}
// EventParams defines the supported event parameters
type EventParams struct {
Name string
Event string
Status int
VirtualPath string
FsPath string
VirtualTargetPath string
FsTargetPath string
ObjectName string
ObjectType string
FileSize int64
Protocol string
IP string
Timestamp int64
Object plugin.Renderer
}
func (p *EventParams) getStringReplacements(addObjectData bool) []string {
replacements := []string{
"{{Name}}", p.Name,
"{{Event}}", p.Event,
"{{Status}}", fmt.Sprintf("%d", p.Status),
"{{VirtualPath}}", p.VirtualPath,
"{{FsPath}}", p.FsPath,
"{{VirtualTargetPath}}", p.VirtualTargetPath,
"{{FsTargetPath}}", p.FsTargetPath,
"{{ObjectName}}", p.ObjectName,
"{{ObjectType}}", p.ObjectType,
"{{FileSize}}", fmt.Sprintf("%d", p.FileSize),
"{{Protocol}}", p.Protocol,
"{{IP}}", p.IP,
"{{Timestamp}}", fmt.Sprintf("%d", p.Timestamp),
}
if addObjectData {
data, err := p.Object.RenderAsJSON(p.Event != operationDelete)
if err == nil {
replacements = append(replacements, "{{ObjectData}}", string(data))
}
}
return replacements
}
func replaceWithReplacer(input string, replacer *strings.Replacer) string {
if !strings.Contains(input, "{{") {
return input
}
return replacer.Replace(input)
}
// checkConditionPatterns returns false if patterns are defined and no match is found
func checkConditionPatterns(name string, patterns []ConditionPattern) bool {
if len(patterns) == 0 {
return true
}
for _, p := range patterns {
if p.match(name) {
return true
}
}
return false
}
func executeSyncActions(rules []EventRule, params EventParams) error {
var errRes error
for _, rule := range rules {
var failedActions []string
for _, action := range rule.Actions {
if !action.Options.IsFailureAction && action.Options.ExecuteSync {
startTime := time.Now()
if err := action.execute(params, rule.Conditions.Options); err != nil {
eventManagerLog(logger.LevelError, "unable to execute sync action %q for rule %q, elapsed %s, err: %v",
action.Name, rule.Name, time.Since(startTime), err)
failedActions = append(failedActions, action.Name)
// we return the last error, it is ok for now
errRes = err
if action.Options.StopOnFailure {
break
}
} else {
eventManagerLog(logger.LevelDebug, "executed sync action %q for rule %q, elapsed: %s",
action.Name, rule.Name, time.Since(startTime))
}
}
}
// execute async actions if any, including failure actions
go executeRuleAsyncActions(rule, params, failedActions)
}
return errRes
}
func executeAsyncActions(rules []EventRule, params EventParams) {
for _, rule := range rules {
executeRuleAsyncActions(rule, params, nil)
}
}
func executeRuleAsyncActions(rule EventRule, params EventParams, failedActions []string) {
for _, action := range rule.Actions {
if !action.Options.IsFailureAction && !action.Options.ExecuteSync {
startTime := time.Now()
if err := action.execute(params, rule.Conditions.Options); err != nil {
eventManagerLog(logger.LevelError, "unable to execute action %q for rule %q, elapsed %s, err: %v",
action.Name, rule.Name, time.Since(startTime), err)
failedActions = append(failedActions, action.Name)
if action.Options.StopOnFailure {
break
}
} else {
eventManagerLog(logger.LevelDebug, "executed action %q for rule %q, elapsed %s",
action.Name, rule.Name, time.Since(startTime))
}
}
if len(failedActions) > 0 {
// execute failure actions
for _, action := range rule.Actions {
if action.Options.IsFailureAction {
startTime := time.Now()
if err := action.execute(params, rule.Conditions.Options); err != nil {
eventManagerLog(logger.LevelError, "unable to execute failure action %q for rule %q, elapsed %s, err: %v",
action.Name, rule.Name, time.Since(startTime), err)
if action.Options.StopOnFailure {
break
}
} else {
eventManagerLog(logger.LevelDebug, "executed failure action %q for rule %q, elapsed: %s",
action.Name, rule.Name, time.Since(startTime))
}
}
}
}
}
}
type cronJob struct {
ruleName string
}
func (j *cronJob) getTask(rule EventRule) (Task, error) {
if rule.guardFromConcurrentExecution() {
task, err := provider.getTaskByName(rule.Name)
if _, ok := err.(*util.RecordNotFoundError); ok {
eventManagerLog(logger.LevelDebug, "adding task for rule %q", rule.Name)
task = Task{
Name: rule.Name,
UpdateAt: 0,
Version: 0,
}
err = provider.addTask(rule.Name)
if err != nil {
eventManagerLog(logger.LevelWarn, "unable to add task for rule %q: %v", rule.Name, err)
return task, err
}
} else {
eventManagerLog(logger.LevelWarn, "unable to get task for rule %q: %v", rule.Name, err)
}
return task, err
}
return Task{}, nil
}
func (j *cronJob) Run() {
eventManagerLog(logger.LevelDebug, "executing scheduled rule %q", j.ruleName)
rule, err := provider.eventRuleExists(j.ruleName)
if err != nil {
eventManagerLog(logger.LevelError, "unable to load rule with name %q", j.ruleName)
return
}
task, err := j.getTask(rule)
if err != nil {
return
}
if task.Name != "" {
updateInterval := 5 * time.Minute
updatedAt := util.GetTimeFromMsecSinceEpoch(task.UpdateAt)
if updatedAt.Add(updateInterval*2 + 1).After(time.Now()) {
eventManagerLog(logger.LevelDebug, "task for rule %q too recent: %s, skip execution", rule.Name, updatedAt)
return
}
err = provider.updateTask(rule.Name, task.Version)
if err != nil {
eventManagerLog(logger.LevelInfo, "unable to update task timestamp for rule %q, skip execution, err: %v",
rule.Name, err)
return
}
ticker := time.NewTicker(updateInterval)
done := make(chan bool)
go func(taskName string) {
eventManagerLog(logger.LevelDebug, "update task %q timestamp worker started", taskName)
for {
select {
case <-done:
eventManagerLog(logger.LevelDebug, "update task %q timestamp worker finished", taskName)
return
case <-ticker.C:
err := provider.updateTaskTimestamp(taskName)
eventManagerLog(logger.LevelInfo, "updated timestamp for task %q, err: %v", taskName, err)
}
}
}(task.Name)
executeRuleAsyncActions(rule, EventParams{}, nil)
done <- true
ticker.Stop()
} else {
executeRuleAsyncActions(rule, EventParams{}, nil)
}
eventManagerLog(logger.LevelDebug, "execution for scheduled rule %q finished", j.ruleName)
}
func cloneKeyValues(keyVals []KeyValue) []KeyValue {
res := make([]KeyValue, 0, len(keyVals))
for _, kv := range keyVals {
res = append(res, KeyValue{
Key: kv.Key,
Value: kv.Value,
})
}
return res
}
func cloneConditionPatterns(patterns []ConditionPattern) []ConditionPattern {
res := make([]ConditionPattern, 0, len(patterns))
for _, p := range patterns {
res = append(res, ConditionPattern{
Pattern: p.Pattern,
InverseMatch: p.InverseMatch,
})
}
return res
}
func eventManagerLog(level logger.LogLevel, format string, v ...any) {
logger.Log(level, "eventmanager", "", format, v...)
}

View file

@ -48,6 +48,14 @@ type memoryProviderHandle struct {
shares map[string]Share shares map[string]Share
// slice with ordered shares shareID // slice with ordered shares shareID
sharesIDs []string sharesIDs []string
// map for event actions, name is the key
actions map[string]BaseEventAction
// slice with ordered actions
actionsNames []string
// map for event actions, name is the key
rules map[string]EventRule
// slice with ordered rules
rulesNames []string
} }
// MemoryProvider defines the auth provider for a memory store // MemoryProvider defines the auth provider for a memory store
@ -78,6 +86,10 @@ func initializeMemoryProvider(basePath string) {
apiKeysIDs: []string{}, apiKeysIDs: []string{},
shares: make(map[string]Share), shares: make(map[string]Share),
sharesIDs: []string{}, sharesIDs: []string{},
actions: make(map[string]BaseEventAction),
actionsNames: []string{},
rules: make(map[string]EventRule),
rulesNames: []string{},
configFile: configFile, configFile: configFile,
}, },
} }
@ -576,14 +588,28 @@ func (p *MemoryProvider) userExistsInternal(username string) (User, error) {
if val, ok := p.dbHandle.users[username]; ok { if val, ok := p.dbHandle.users[username]; ok {
return val.getACopy(), nil return val.getACopy(), nil
} }
return User{}, util.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist", username)) return User{}, util.NewRecordNotFoundError(fmt.Sprintf("username %q does not exist", username))
} }
func (p *MemoryProvider) groupExistsInternal(name string) (Group, error) { func (p *MemoryProvider) groupExistsInternal(name string) (Group, error) {
if val, ok := p.dbHandle.groups[name]; ok { if val, ok := p.dbHandle.groups[name]; ok {
return val.getACopy(), nil return val.getACopy(), nil
} }
return Group{}, util.NewRecordNotFoundError(fmt.Sprintf("group %#v does not exist", name)) return Group{}, util.NewRecordNotFoundError(fmt.Sprintf("group %q does not exist", name))
}
func (p *MemoryProvider) actionExistsInternal(name string) (BaseEventAction, error) {
if val, ok := p.dbHandle.actions[name]; ok {
return val.getACopy(), nil
}
return BaseEventAction{}, util.NewRecordNotFoundError(fmt.Sprintf("event action %q does not exist", name))
}
func (p *MemoryProvider) ruleExistsInternal(name string) (EventRule, error) {
if val, ok := p.dbHandle.rules[name]; ok {
return val.getACopy(), nil
}
return EventRule{}, util.NewRecordNotFoundError(fmt.Sprintf("event rule %q does not exist", name))
} }
func (p *MemoryProvider) addAdmin(admin *Admin) error { func (p *MemoryProvider) addAdmin(admin *Admin) error {
@ -983,6 +1009,52 @@ func (p *MemoryProvider) addVirtualFoldersToGroup(group *Group) {
} }
} }
func (p *MemoryProvider) addActionsToRule(rule *EventRule) {
var actions []EventAction
for idx := range rule.Actions {
action := &rule.Actions[idx]
baseAction, err := p.actionExistsInternal(action.Name)
if err != nil {
continue
}
baseAction.Options.SetEmptySecretsIfNil()
action.BaseEventAction = baseAction
actions = append(actions, *action)
}
rule.Actions = actions
}
func (p *MemoryProvider) addRuleToActionMapping(ruleName, actionName string) error {
a, err := p.actionExistsInternal(actionName)
if err != nil {
return util.NewGenericError(fmt.Sprintf("action %q does not exist", actionName))
}
if !util.Contains(a.Rules, ruleName) {
a.Rules = append(a.Rules, ruleName)
p.dbHandle.actions[actionName] = a
}
return nil
}
func (p *MemoryProvider) removeRuleFromActionMapping(ruleName, actionName string) error {
a, err := p.actionExistsInternal(actionName)
if err != nil {
providerLog(logger.LevelWarn, "action %q does not exist, cannot remove from mapping", actionName)
return nil
}
if util.Contains(a.Rules, ruleName) {
var rules []string
for _, r := range a.Rules {
if r != ruleName {
rules = append(rules, r)
}
}
a.Rules = rules
p.dbHandle.actions[actionName] = a
}
return nil
}
func (p *MemoryProvider) addUserFromGroupMapping(username, groupname string) error { func (p *MemoryProvider) addUserFromGroupMapping(username, groupname string) error {
g, err := p.groupExistsInternal(groupname) g, err := p.groupExistsInternal(groupname)
if err != nil { if err != nil {
@ -1768,6 +1840,359 @@ func (p *MemoryProvider) cleanupSharedSessions(sessionType SessionType, before i
return ErrNotImplemented return ErrNotImplemented
} }
func (p *MemoryProvider) getEventActions(limit, offset int, order string, minimal bool) ([]BaseEventAction, error) {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return nil, errMemoryProviderClosed
}
if limit <= 0 {
return nil, nil
}
actions := make([]BaseEventAction, 0, limit)
itNum := 0
if order == OrderASC {
for _, name := range p.dbHandle.actionsNames {
itNum++
if itNum <= offset {
continue
}
a := p.dbHandle.actions[name]
action := a.getACopy()
action.PrepareForRendering()
actions = append(actions, action)
if len(actions) >= limit {
break
}
}
} else {
for i := len(p.dbHandle.actionsNames) - 1; i >= 0; i-- {
itNum++
if itNum <= offset {
continue
}
name := p.dbHandle.actionsNames[i]
a := p.dbHandle.actions[name]
action := a.getACopy()
action.PrepareForRendering()
actions = append(actions, action)
if len(actions) >= limit {
break
}
}
}
return actions, nil
}
func (p *MemoryProvider) dumpEventActions() ([]BaseEventAction, error) {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return nil, errMemoryProviderClosed
}
actions := make([]BaseEventAction, 0, len(p.dbHandle.actions))
for _, name := range p.dbHandle.actionsNames {
a := p.dbHandle.actions[name]
action := a.getACopy()
actions = append(actions, action)
}
return actions, nil
}
func (p *MemoryProvider) eventActionExists(name string) (BaseEventAction, error) {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return BaseEventAction{}, errMemoryProviderClosed
}
return p.actionExistsInternal(name)
}
func (p *MemoryProvider) addEventAction(action *BaseEventAction) error {
err := action.validate()
if err != nil {
return err
}
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return errMemoryProviderClosed
}
_, err = p.actionExistsInternal(action.Name)
if err == nil {
return fmt.Errorf("event action %q already exists", action.Name)
}
action.ID = p.getNextActionID()
action.Rules = nil
p.dbHandle.actions[action.Name] = action.getACopy()
p.dbHandle.actionsNames = append(p.dbHandle.actionsNames, action.Name)
sort.Strings(p.dbHandle.actionsNames)
return nil
}
func (p *MemoryProvider) updateEventAction(action *BaseEventAction) error {
err := action.validate()
if err != nil {
return err
}
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return errMemoryProviderClosed
}
oldAction, err := p.actionExistsInternal(action.Name)
if err != nil {
return fmt.Errorf("event action %s does not exist", action.Name)
}
action.ID = oldAction.ID
action.Name = oldAction.Name
action.Rules = nil
if len(oldAction.Rules) > 0 {
var relatedRules []string
for _, ruleName := range oldAction.Rules {
rule, err := p.ruleExistsInternal(ruleName)
if err == nil {
relatedRules = append(relatedRules, ruleName)
rule.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
p.dbHandle.rules[ruleName] = rule
setLastRuleUpdate()
}
}
action.Rules = relatedRules
}
p.dbHandle.actions[action.Name] = action.getACopy()
return nil
}
func (p *MemoryProvider) deleteEventAction(action BaseEventAction) error {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return errMemoryProviderClosed
}
oldAction, err := p.actionExistsInternal(action.Name)
if err != nil {
return fmt.Errorf("event action %s does not exist", action.Name)
}
if len(oldAction.Rules) > 0 {
return util.NewValidationError(fmt.Sprintf("action %s is referenced, it cannot be removed", oldAction.Name))
}
delete(p.dbHandle.actions, action.Name)
// this could be more efficient
p.dbHandle.actionsNames = make([]string, 0, len(p.dbHandle.actions))
for name := range p.dbHandle.actions {
p.dbHandle.actionsNames = append(p.dbHandle.actionsNames, name)
}
sort.Strings(p.dbHandle.actionsNames)
return nil
}
func (p *MemoryProvider) getEventRules(limit, offset int, order string) ([]EventRule, error) {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return nil, errMemoryProviderClosed
}
if limit <= 0 {
return nil, nil
}
itNum := 0
rules := make([]EventRule, 0, limit)
if order == OrderASC {
for _, name := range p.dbHandle.rulesNames {
itNum++
if itNum <= offset {
continue
}
r := p.dbHandle.rules[name]
rule := r.getACopy()
p.addActionsToRule(&rule)
rule.PrepareForRendering()
rules = append(rules, rule)
if len(rules) >= limit {
break
}
}
} else {
for i := len(p.dbHandle.rulesNames) - 1; i >= 0; i-- {
itNum++
if itNum <= offset {
continue
}
name := p.dbHandle.rulesNames[i]
r := p.dbHandle.rules[name]
rule := r.getACopy()
p.addActionsToRule(&rule)
rule.PrepareForRendering()
rules = append(rules, rule)
if len(rules) >= limit {
break
}
}
}
return rules, nil
}
func (p *MemoryProvider) dumpEventRules() ([]EventRule, error) {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return nil, errMemoryProviderClosed
}
rules := make([]EventRule, 0, len(p.dbHandle.rules))
for _, name := range p.dbHandle.rulesNames {
r := p.dbHandle.rules[name]
rule := r.getACopy()
p.addActionsToRule(&rule)
rules = append(rules, rule)
}
return rules, nil
}
func (p *MemoryProvider) getRecentlyUpdatedRules(after int64) ([]EventRule, error) {
if getLastRuleUpdate() < after {
return nil, nil
}
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return nil, errMemoryProviderClosed
}
rules := make([]EventRule, 0, 10)
for _, name := range p.dbHandle.rulesNames {
r := p.dbHandle.rules[name]
if r.UpdatedAt < after {
continue
}
rule := r.getACopy()
p.addActionsToRule(&rule)
rules = append(rules, rule)
}
return rules, nil
}
func (p *MemoryProvider) eventRuleExists(name string) (EventRule, error) {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return EventRule{}, errMemoryProviderClosed
}
rule, err := p.ruleExistsInternal(name)
if err != nil {
return rule, err
}
p.addActionsToRule(&rule)
return rule, nil
}
func (p *MemoryProvider) addEventRule(rule *EventRule) error {
if err := rule.validate(); err != nil {
return err
}
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return errMemoryProviderClosed
}
_, err := p.ruleExistsInternal(rule.Name)
if err == nil {
return fmt.Errorf("event rule %q already exists", rule.Name)
}
rule.ID = p.getNextRuleID()
rule.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
rule.UpdatedAt = rule.CreatedAt
for idx := range rule.Actions {
if err := p.addRuleToActionMapping(rule.Name, rule.Actions[idx].Name); err != nil {
return err
}
}
sort.Slice(rule.Actions, func(i, j int) bool {
return rule.Actions[i].Order < rule.Actions[j].Order
})
p.dbHandle.rules[rule.Name] = rule.getACopy()
p.dbHandle.rulesNames = append(p.dbHandle.rulesNames, rule.Name)
sort.Strings(p.dbHandle.rulesNames)
setLastRuleUpdate()
return nil
}
func (p *MemoryProvider) updateEventRule(rule *EventRule) error {
if err := rule.validate(); err != nil {
return err
}
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return errMemoryProviderClosed
}
oldRule, err := p.ruleExistsInternal(rule.Name)
if err != nil {
return err
}
for idx := range oldRule.Actions {
if err = p.removeRuleFromActionMapping(rule.Name, oldRule.Actions[idx].Name); err != nil {
return err
}
}
for idx := range rule.Actions {
if err = p.addRuleToActionMapping(rule.Name, rule.Actions[idx].Name); err != nil {
return err
}
}
rule.ID = oldRule.ID
rule.CreatedAt = oldRule.CreatedAt
rule.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
sort.Slice(rule.Actions, func(i, j int) bool {
return rule.Actions[i].Order < rule.Actions[j].Order
})
p.dbHandle.rules[rule.Name] = rule.getACopy()
setLastRuleUpdate()
return nil
}
func (p *MemoryProvider) deleteEventRule(rule EventRule, softDelete bool) error {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return errMemoryProviderClosed
}
oldRule, err := p.ruleExistsInternal(rule.Name)
if err != nil {
return err
}
if len(oldRule.Actions) > 0 {
for idx := range oldRule.Actions {
if err = p.removeRuleFromActionMapping(rule.Name, oldRule.Actions[idx].Name); err != nil {
return err
}
}
}
delete(p.dbHandle.rules, rule.Name)
p.dbHandle.rulesNames = make([]string, 0, len(p.dbHandle.rules))
for name := range p.dbHandle.rules {
p.dbHandle.rulesNames = append(p.dbHandle.rulesNames, name)
}
sort.Strings(p.dbHandle.rulesNames)
setLastRuleUpdate()
return nil
}
func (p *MemoryProvider) getTaskByName(name string) (Task, error) {
return Task{}, ErrNotImplemented
}
func (p *MemoryProvider) addTask(name string) error {
return ErrNotImplemented
}
func (p *MemoryProvider) updateTask(name string, version int64) error {
return ErrNotImplemented
}
func (p *MemoryProvider) updateTaskTimestamp(name string) error {
return ErrNotImplemented
}
func (p *MemoryProvider) getNextID() int64 { func (p *MemoryProvider) getNextID() int64 {
nextID := int64(1) nextID := int64(1)
for _, v := range p.dbHandle.users { for _, v := range p.dbHandle.users {
@ -1808,6 +2233,26 @@ func (p *MemoryProvider) getNextGroupID() int64 {
return nextID return nextID
} }
func (p *MemoryProvider) getNextActionID() int64 {
nextID := int64(1)
for _, a := range p.dbHandle.actions {
if a.ID >= nextID {
nextID = a.ID + 1
}
}
return nextID
}
func (p *MemoryProvider) getNextRuleID() int64 {
nextID := int64(1)
for _, r := range p.dbHandle.rules {
if r.ID >= nextID {
nextID = r.ID + 1
}
}
return nextID
}
func (p *MemoryProvider) clear() { func (p *MemoryProvider) clear() {
p.dbHandle.Lock() p.dbHandle.Lock()
defer p.dbHandle.Unlock() defer p.dbHandle.Unlock()
@ -1856,27 +2301,35 @@ func (p *MemoryProvider) reloadConfig() error {
} }
p.clear() p.clear()
if err := p.restoreFolders(&dump); err != nil { if err := p.restoreFolders(dump); err != nil {
return err return err
} }
if err := p.restoreGroups(&dump); err != nil { if err := p.restoreGroups(dump); err != nil {
return err return err
} }
if err := p.restoreUsers(&dump); err != nil { if err := p.restoreUsers(dump); err != nil {
return err return err
} }
if err := p.restoreAdmins(&dump); err != nil { if err := p.restoreAdmins(dump); err != nil {
return err return err
} }
if err := p.restoreAPIKeys(&dump); err != nil { if err := p.restoreAPIKeys(dump); err != nil {
return err return err
} }
if err := p.restoreShares(&dump); err != nil { if err := p.restoreShares(dump); err != nil {
return err
}
if err := p.restoreEventActions(dump); err != nil {
return err
}
if err := p.restoreEventRules(dump); err != nil {
return err return err
} }
@ -1884,7 +2337,51 @@ func (p *MemoryProvider) reloadConfig() error {
return nil return nil
} }
func (p *MemoryProvider) restoreShares(dump *BackupData) error { func (p *MemoryProvider) restoreEventActions(dump BackupData) error {
for _, action := range dump.EventActions {
a, err := p.eventActionExists(action.Name)
action := action // pin
if err == nil {
action.ID = a.ID
err = UpdateEventAction(&action, ActionExecutorSystem, "")
if err != nil {
providerLog(logger.LevelError, "error updating event action %q: %v", action.Name, err)
return err
}
} else {
err = AddEventAction(&action, ActionExecutorSystem, "")
if err != nil {
providerLog(logger.LevelError, "error adding event action %q: %v", action.Name, err)
return err
}
}
}
return nil
}
func (p *MemoryProvider) restoreEventRules(dump BackupData) error {
for _, rule := range dump.EventRules {
r, err := p.eventRuleExists(rule.Name)
rule := rule // pin
if err == nil {
rule.ID = r.ID
err = UpdateEventRule(&rule, ActionExecutorSystem, "")
if err != nil {
providerLog(logger.LevelError, "error updating event rule %q: %v", rule.Name, err)
return err
}
} else {
err = AddEventRule(&rule, ActionExecutorSystem, "")
if err != nil {
providerLog(logger.LevelError, "error adding event rule %q: %v", rule.Name, err)
return err
}
}
}
return nil
}
func (p *MemoryProvider) restoreShares(dump BackupData) error {
for _, share := range dump.Shares { for _, share := range dump.Shares {
s, err := p.shareExists(share.ShareID, "") s, err := p.shareExists(share.ShareID, "")
share := share // pin share := share // pin
@ -1907,7 +2404,7 @@ func (p *MemoryProvider) restoreShares(dump *BackupData) error {
return nil return nil
} }
func (p *MemoryProvider) restoreAPIKeys(dump *BackupData) error { func (p *MemoryProvider) restoreAPIKeys(dump BackupData) error {
for _, apiKey := range dump.APIKeys { for _, apiKey := range dump.APIKeys {
if apiKey.Key == "" { if apiKey.Key == "" {
return fmt.Errorf("cannot restore an empty API key: %+v", apiKey) return fmt.Errorf("cannot restore an empty API key: %+v", apiKey)
@ -1932,7 +2429,7 @@ func (p *MemoryProvider) restoreAPIKeys(dump *BackupData) error {
return nil return nil
} }
func (p *MemoryProvider) restoreAdmins(dump *BackupData) error { func (p *MemoryProvider) restoreAdmins(dump BackupData) error {
for _, admin := range dump.Admins { for _, admin := range dump.Admins {
admin := admin // pin admin := admin // pin
admin.Username = config.convertName(admin.Username) admin.Username = config.convertName(admin.Username)
@ -1955,7 +2452,7 @@ func (p *MemoryProvider) restoreAdmins(dump *BackupData) error {
return nil return nil
} }
func (p *MemoryProvider) restoreGroups(dump *BackupData) error { func (p *MemoryProvider) restoreGroups(dump BackupData) error {
for _, group := range dump.Groups { for _, group := range dump.Groups {
group := group // pin group := group // pin
group.Name = config.convertName(group.Name) group.Name = config.convertName(group.Name)
@ -1979,7 +2476,7 @@ func (p *MemoryProvider) restoreGroups(dump *BackupData) error {
return nil return nil
} }
func (p *MemoryProvider) restoreFolders(dump *BackupData) error { func (p *MemoryProvider) restoreFolders(dump BackupData) error {
for _, folder := range dump.Folders { for _, folder := range dump.Folders {
folder := folder // pin folder := folder // pin
folder.Name = config.convertName(folder.Name) folder.Name = config.convertName(folder.Name)
@ -2003,7 +2500,7 @@ func (p *MemoryProvider) restoreFolders(dump *BackupData) error {
return nil return nil
} }
func (p *MemoryProvider) restoreUsers(dump *BackupData) error { func (p *MemoryProvider) restoreUsers(dump BackupData) error {
for _, user := range dump.Users { for _, user := range dump.Users {
user := user // pin user := user // pin
user.Username = config.convertName(user.Username) user.Username = config.convertName(user.Username)

View file

@ -36,6 +36,10 @@ const (
"DROP TABLE IF EXISTS `{{defender_hosts}}` CASCADE;" + "DROP TABLE IF EXISTS `{{defender_hosts}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{active_transfers}}` CASCADE;" + "DROP TABLE IF EXISTS `{{active_transfers}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{shared_sessions}}` CASCADE;" + "DROP TABLE IF EXISTS `{{shared_sessions}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{rules_actions_mapping}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{events_actions}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{events_rules}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{tasks}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{schema_version}}` CASCADE;" "DROP TABLE IF EXISTS `{{schema_version}}` CASCADE;"
mysqlInitialSQL = "CREATE TABLE `{{schema_version}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `version` integer NOT NULL);" + mysqlInitialSQL = "CREATE TABLE `{{schema_version}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `version` integer NOT NULL);" +
"CREATE TABLE `{{admins}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `username` varchar(255) NOT NULL UNIQUE, " + "CREATE TABLE `{{admins}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `username` varchar(255) NOT NULL UNIQUE, " +
@ -119,6 +123,33 @@ const (
"CREATE INDEX `{{prefix}}shared_sessions_type_idx` ON `{{shared_sessions}}` (`type`);" + "CREATE INDEX `{{prefix}}shared_sessions_type_idx` ON `{{shared_sessions}}` (`type`);" +
"CREATE INDEX `{{prefix}}shared_sessions_timestamp_idx` ON `{{shared_sessions}}` (`timestamp`);" + "CREATE INDEX `{{prefix}}shared_sessions_timestamp_idx` ON `{{shared_sessions}}` (`timestamp`);" +
"INSERT INTO {{schema_version}} (version) VALUES (19);" "INSERT INTO {{schema_version}} (version) VALUES (19);"
mysqlV20SQL = "CREATE TABLE `{{events_rules}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, " +
"`name` varchar(255) NOT NULL UNIQUE, `description` varchar(512) NULL, `created_at` bigint NOT NULL, " +
"`updated_at` bigint NOT NULL, `trigger` integer NOT NULL, `conditions` longtext NOT NULL, `deleted_at` bigint NOT NULL);" +
"CREATE TABLE `{{events_actions}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, " +
"`name` varchar(255) NOT NULL UNIQUE, `description` varchar(512) NULL, `type` integer NOT NULL, " +
"`options` longtext NOT NULL);" +
"CREATE TABLE `{{rules_actions_mapping}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, " +
"`rule_id` integer NOT NULL, `action_id` integer NOT NULL, `order` integer NOT NULL, `options` longtext NOT NULL);" +
"CREATE TABLE `{{tasks}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` varchar(255) NOT NULL UNIQUE, " +
"`updated_at` bigint NOT NULL, `version` bigint NOT NULL);" +
"ALTER TABLE `{{rules_actions_mapping}}` ADD CONSTRAINT `{{prefix}}unique_rule_action_mapping` UNIQUE (`rule_id`, `action_id`);" +
"ALTER TABLE `{{rules_actions_mapping}}` ADD CONSTRAINT `{{prefix}}rules_actions_mapping_rule_id_fk_events_rules_id` " +
"FOREIGN KEY (`rule_id`) REFERENCES `{{events_rules}}` (`id`) ON DELETE CASCADE;" +
"ALTER TABLE `{{rules_actions_mapping}}` ADD CONSTRAINT `{{prefix}}rules_actions_mapping_action_id_fk_events_targets_id` " +
"FOREIGN KEY (`action_id`) REFERENCES `{{events_actions}}` (`id`) ON DELETE NO ACTION;" +
"ALTER TABLE `{{users}}` ADD COLUMN `deleted_at` bigint DEFAULT 0 NOT NULL;" +
"ALTER TABLE `{{users}}` ALTER COLUMN `deleted_at` DROP DEFAULT;" +
"CREATE INDEX `{{prefix}}events_rules_updated_at_idx` ON `{{events_rules}}` (`updated_at`);" +
"CREATE INDEX `{{prefix}}events_rules_deleted_at_idx` ON `{{events_rules}}` (`deleted_at`);" +
"CREATE INDEX `{{prefix}}events_rules_trigger_idx` ON `{{events_rules}}` (`trigger`);" +
"CREATE INDEX `{{prefix}}rules_actions_mapping_order_idx` ON `{{rules_actions_mapping}}` (`order`);" +
"CREATE INDEX `{{prefix}}users_deleted_at_idx` ON `{{users}}` (`deleted_at`);"
mysqlV20DownSQL = "DROP TABLE `{{rules_actions_mapping}}` CASCADE;" +
"DROP TABLE `{{events_rules}}` CASCADE;" +
"DROP TABLE `{{events_actions}}` CASCADE;" +
"DROP TABLE `{{tasks}}` CASCADE;" +
"ALTER TABLE `{{users}}` DROP COLUMN `deleted_at`;"
) )
// MySQLProvider defines the auth provider for MySQL/MariaDB database // MySQLProvider defines the auth provider for MySQL/MariaDB database
@ -503,6 +534,74 @@ func (p *MySQLProvider) cleanupSharedSessions(sessionType SessionType, before in
return sqlCommonCleanupSessions(sessionType, before, p.dbHandle) return sqlCommonCleanupSessions(sessionType, before, p.dbHandle)
} }
func (p *MySQLProvider) getEventActions(limit, offset int, order string, minimal bool) ([]BaseEventAction, error) {
return sqlCommonGetEventActions(limit, offset, order, minimal, p.dbHandle)
}
func (p *MySQLProvider) dumpEventActions() ([]BaseEventAction, error) {
return sqlCommonDumpEventActions(p.dbHandle)
}
func (p *MySQLProvider) eventActionExists(name string) (BaseEventAction, error) {
return sqlCommonGetEventActionByName(name, p.dbHandle)
}
func (p *MySQLProvider) addEventAction(action *BaseEventAction) error {
return sqlCommonAddEventAction(action, p.dbHandle)
}
func (p *MySQLProvider) updateEventAction(action *BaseEventAction) error {
return sqlCommonUpdateEventAction(action, p.dbHandle)
}
func (p *MySQLProvider) deleteEventAction(action BaseEventAction) error {
return sqlCommonDeleteEventAction(action, p.dbHandle)
}
func (p *MySQLProvider) getEventRules(limit, offset int, order string) ([]EventRule, error) {
return sqlCommonGetEventRules(limit, offset, order, p.dbHandle)
}
func (p *MySQLProvider) dumpEventRules() ([]EventRule, error) {
return sqlCommonDumpEventRules(p.dbHandle)
}
func (p *MySQLProvider) getRecentlyUpdatedRules(after int64) ([]EventRule, error) {
return sqlCommonGetRecentlyUpdatedRules(after, p.dbHandle)
}
func (p *MySQLProvider) eventRuleExists(name string) (EventRule, error) {
return sqlCommonGetEventRuleByName(name, p.dbHandle)
}
func (p *MySQLProvider) addEventRule(rule *EventRule) error {
return sqlCommonAddEventRule(rule, p.dbHandle)
}
func (p *MySQLProvider) updateEventRule(rule *EventRule) error {
return sqlCommonUpdateEventRule(rule, p.dbHandle)
}
func (p *MySQLProvider) deleteEventRule(rule EventRule, softDelete bool) error {
return sqlCommonDeleteEventRule(rule, softDelete, p.dbHandle)
}
func (p *MySQLProvider) getTaskByName(name string) (Task, error) {
return sqlCommonGetTaskByName(name, p.dbHandle)
}
func (p *MySQLProvider) addTask(name string) error {
return sqlCommonAddTask(name, p.dbHandle)
}
func (p *MySQLProvider) updateTask(name string, version int64) error {
return sqlCommonUpdateTask(name, version, p.dbHandle)
}
func (p *MySQLProvider) updateTaskTimestamp(name string) error {
return sqlCommonUpdateTaskTimestamp(name, p.dbHandle)
}
func (p *MySQLProvider) close() error { func (p *MySQLProvider) close() error {
return p.dbHandle.Close() return p.dbHandle.Close()
} }
@ -542,6 +641,8 @@ func (p *MySQLProvider) migrateDatabase() error { //nolint:dupl
providerLog(logger.LevelError, "%v", err) providerLog(logger.LevelError, "%v", err)
logger.ErrorToConsole("%v", err) logger.ErrorToConsole("%v", err)
return err return err
case version == 19:
return updateMySQLDatabaseFromV19(p.dbHandle)
default: default:
if version > sqlDatabaseVersion { if version > sqlDatabaseVersion {
providerLog(logger.LevelError, "database version %v is newer than the supported one: %v", version, providerLog(logger.LevelError, "database version %v is newer than the supported one: %v", version,
@ -564,6 +665,8 @@ func (p *MySQLProvider) revertDatabase(targetVersion int) error {
} }
switch dbVersion.Version { switch dbVersion.Version {
case 20:
return downgradeMySQLDatabaseFromV20(p.dbHandle)
default: default:
return fmt.Errorf("database version not handled: %v", dbVersion.Version) return fmt.Errorf("database version not handled: %v", dbVersion.Version)
} }
@ -573,3 +676,34 @@ func (p *MySQLProvider) resetDatabase() error {
sql := sqlReplaceAll(mysqlResetSQL) sql := sqlReplaceAll(mysqlResetSQL)
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, strings.Split(sql, ";"), 0, false) return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, strings.Split(sql, ";"), 0, false)
} }
func updateMySQLDatabaseFromV19(dbHandle *sql.DB) error {
return updateMySQLDatabaseFrom19To20(dbHandle)
}
func downgradeMySQLDatabaseFromV20(dbHandle *sql.DB) error {
return downgradeMySQLDatabaseFrom20To19(dbHandle)
}
func updateMySQLDatabaseFrom19To20(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 19 -> 20")
providerLog(logger.LevelInfo, "updating database version: 19 -> 20")
sql := strings.ReplaceAll(mysqlV20SQL, "{{events_actions}}", sqlTableEventsActions)
sql = strings.ReplaceAll(sql, "{{events_rules}}", sqlTableEventsRules)
sql = strings.ReplaceAll(sql, "{{rules_actions_mapping}}", sqlTableRulesActionsMapping)
sql = strings.ReplaceAll(sql, "{{users}}", sqlTableUsers)
sql = strings.ReplaceAll(sql, "{{tasks}}", sqlTableTasks)
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 20, true)
}
func downgradeMySQLDatabaseFrom20To19(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 20 -> 19")
providerLog(logger.LevelInfo, "downgrading database version: 20 -> 19")
sql := strings.ReplaceAll(mysqlV20DownSQL, "{{events_actions}}", sqlTableEventsActions)
sql = strings.ReplaceAll(sql, "{{events_rules}}", sqlTableEventsRules)
sql = strings.ReplaceAll(sql, "{{rules_actions_mapping}}", sqlTableRulesActionsMapping)
sql = strings.ReplaceAll(sql, "{{users}}", sqlTableUsers)
sql = strings.ReplaceAll(sql, "{{tasks}}", sqlTableTasks)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 19, false)
}

View file

@ -35,6 +35,10 @@ DROP TABLE IF EXISTS "{{defender_events}}" CASCADE;
DROP TABLE IF EXISTS "{{defender_hosts}}" CASCADE; DROP TABLE IF EXISTS "{{defender_hosts}}" CASCADE;
DROP TABLE IF EXISTS "{{active_transfers}}" CASCADE; DROP TABLE IF EXISTS "{{active_transfers}}" CASCADE;
DROP TABLE IF EXISTS "{{shared_sessions}}" CASCADE; DROP TABLE IF EXISTS "{{shared_sessions}}" CASCADE;
DROP TABLE IF EXISTS "{{rules_actions_mapping}}" CASCADE;
DROP TABLE IF EXISTS "{{events_actions}}" CASCADE;
DROP TABLE IF EXISTS "{{events_rules}}" CASCADE;
DROP TABLE IF EXISTS "{{tasks}}" CASCADE;
DROP TABLE IF EXISTS "{{schema_version}}" CASCADE; DROP TABLE IF EXISTS "{{schema_version}}" CASCADE;
` `
pgsqlInitial = `CREATE TABLE "{{schema_version}}" ("id" serial NOT NULL PRIMARY KEY, "version" integer NOT NULL); pgsqlInitial = `CREATE TABLE "{{schema_version}}" ("id" serial NOT NULL PRIMARY KEY, "version" integer NOT NULL);
@ -127,6 +131,36 @@ CREATE INDEX "{{prefix}}active_transfers_updated_at_idx" ON "{{active_transfers}
CREATE INDEX "{{prefix}}shared_sessions_type_idx" ON "{{shared_sessions}}" ("type"); CREATE INDEX "{{prefix}}shared_sessions_type_idx" ON "{{shared_sessions}}" ("type");
CREATE INDEX "{{prefix}}shared_sessions_timestamp_idx" ON "{{shared_sessions}}" ("timestamp"); CREATE INDEX "{{prefix}}shared_sessions_timestamp_idx" ON "{{shared_sessions}}" ("timestamp");
INSERT INTO {{schema_version}} (version) VALUES (19); INSERT INTO {{schema_version}} (version) VALUES (19);
`
pgsqlV20SQL = `CREATE TABLE "{{events_rules}}" ("id" serial NOT NULL PRIMARY KEY, "name" varchar(255) NOT NULL UNIQUE,
"description" varchar(512) NULL, "created_at" bigint NOT NULL, "updated_at" bigint NOT NULL, "trigger" integer NOT NULL,
"conditions" text NOT NULL, "deleted_at" bigint NOT NULL);
CREATE TABLE "{{events_actions}}" ("id" serial NOT NULL PRIMARY KEY, "name" varchar(255) NOT NULL UNIQUE,
"description" varchar(512) NULL, "type" integer NOT NULL, "options" text NOT NULL);
CREATE TABLE "{{rules_actions_mapping}}" ("id" serial NOT NULL PRIMARY KEY, "rule_id" integer NOT NULL,
"action_id" integer NOT NULL, "order" integer NOT NULL, "options" text NOT NULL);
CREATE TABLE "{{tasks}}" ("id" serial NOT NULL PRIMARY KEY, "name" varchar(255) NOT NULL UNIQUE, "updated_at" bigint NOT NULL,
"version" bigint NOT NULL);
ALTER TABLE "{{rules_actions_mapping}}" ADD CONSTRAINT "{{prefix}}unique_rule_action_mapping" UNIQUE ("rule_id", "action_id");
ALTER TABLE "{{rules_actions_mapping}}" ADD CONSTRAINT "{{prefix}}rules_actions_mapping_rule_id_fk_events_rules_id"
FOREIGN KEY ("rule_id") REFERENCES "{{events_rules}}" ("id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;
ALTER TABLE "{{rules_actions_mapping}}" ADD CONSTRAINT "{{prefix}}rules_actions_mapping_action_id_fk_events_targets_id"
FOREIGN KEY ("action_id") REFERENCES "{{events_actions}}" ("id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION;
ALTER TABLE "{{users}}" ADD COLUMN "deleted_at" bigint DEFAULT 0 NOT NULL;
ALTER TABLE "{{users}}" ALTER COLUMN "deleted_at" DROP DEFAULT;
CREATE INDEX "{{prefix}}events_rules_updated_at_idx" ON "{{events_rules}}" ("updated_at");
CREATE INDEX "{{prefix}}events_rules_deleted_at_idx" ON "{{events_rules}}" ("deleted_at");
CREATE INDEX "{{prefix}}events_rules_trigger_idx" ON "{{events_rules}}" ("trigger");
CREATE INDEX "{{prefix}}rules_actions_mapping_rule_id_idx" ON "{{rules_actions_mapping}}" ("rule_id");
CREATE INDEX "{{prefix}}rules_actions_mapping_action_id_idx" ON "{{rules_actions_mapping}}" ("action_id");
CREATE INDEX "{{prefix}}rules_actions_mapping_order_idx" ON "{{rules_actions_mapping}}" ("order");
CREATE INDEX "{{prefix}}users_deleted_at_idx" ON "{{users}}" ("deleted_at");
`
pgsqlV20DownSQL = `DROP TABLE "{{rules_actions_mapping}}" CASCADE;
DROP TABLE "{{events_rules}}" CASCADE;
DROP TABLE "{{events_actions}}" CASCADE;
DROP TABLE "{{tasks}}" CASCADE;
ALTER TABLE "{{users}}" DROP COLUMN "deleted_at" CASCADE;
` `
) )
@ -475,6 +509,74 @@ func (p *PGSQLProvider) cleanupSharedSessions(sessionType SessionType, before in
return sqlCommonCleanupSessions(sessionType, before, p.dbHandle) return sqlCommonCleanupSessions(sessionType, before, p.dbHandle)
} }
func (p *PGSQLProvider) getEventActions(limit, offset int, order string, minimal bool) ([]BaseEventAction, error) {
return sqlCommonGetEventActions(limit, offset, order, minimal, p.dbHandle)
}
func (p *PGSQLProvider) dumpEventActions() ([]BaseEventAction, error) {
return sqlCommonDumpEventActions(p.dbHandle)
}
func (p *PGSQLProvider) eventActionExists(name string) (BaseEventAction, error) {
return sqlCommonGetEventActionByName(name, p.dbHandle)
}
func (p *PGSQLProvider) addEventAction(action *BaseEventAction) error {
return sqlCommonAddEventAction(action, p.dbHandle)
}
func (p *PGSQLProvider) updateEventAction(action *BaseEventAction) error {
return sqlCommonUpdateEventAction(action, p.dbHandle)
}
func (p *PGSQLProvider) deleteEventAction(action BaseEventAction) error {
return sqlCommonDeleteEventAction(action, p.dbHandle)
}
func (p *PGSQLProvider) getEventRules(limit, offset int, order string) ([]EventRule, error) {
return sqlCommonGetEventRules(limit, offset, order, p.dbHandle)
}
func (p *PGSQLProvider) dumpEventRules() ([]EventRule, error) {
return sqlCommonDumpEventRules(p.dbHandle)
}
func (p *PGSQLProvider) getRecentlyUpdatedRules(after int64) ([]EventRule, error) {
return sqlCommonGetRecentlyUpdatedRules(after, p.dbHandle)
}
func (p *PGSQLProvider) eventRuleExists(name string) (EventRule, error) {
return sqlCommonGetEventRuleByName(name, p.dbHandle)
}
func (p *PGSQLProvider) addEventRule(rule *EventRule) error {
return sqlCommonAddEventRule(rule, p.dbHandle)
}
func (p *PGSQLProvider) updateEventRule(rule *EventRule) error {
return sqlCommonUpdateEventRule(rule, p.dbHandle)
}
func (p *PGSQLProvider) deleteEventRule(rule EventRule, softDelete bool) error {
return sqlCommonDeleteEventRule(rule, softDelete, p.dbHandle)
}
func (p *PGSQLProvider) getTaskByName(name string) (Task, error) {
return sqlCommonGetTaskByName(name, p.dbHandle)
}
func (p *PGSQLProvider) addTask(name string) error {
return sqlCommonAddTask(name, p.dbHandle)
}
func (p *PGSQLProvider) updateTask(name string, version int64) error {
return sqlCommonUpdateTask(name, version, p.dbHandle)
}
func (p *PGSQLProvider) updateTaskTimestamp(name string) error {
return sqlCommonUpdateTaskTimestamp(name, p.dbHandle)
}
func (p *PGSQLProvider) close() error { func (p *PGSQLProvider) close() error {
return p.dbHandle.Close() return p.dbHandle.Close()
} }
@ -495,12 +597,6 @@ func (p *PGSQLProvider) initializeDatabase() error {
logger.InfoToConsole("creating initial database schema, version 19") logger.InfoToConsole("creating initial database schema, version 19")
providerLog(logger.LevelInfo, "creating initial database schema, version 19") providerLog(logger.LevelInfo, "creating initial database schema, version 19")
initialSQL := sqlReplaceAll(pgsqlInitial) initialSQL := sqlReplaceAll(pgsqlInitial)
if config.Driver == CockroachDataProviderName {
// Cockroach does not support deferrable constraint validation, we don't need them,
// we keep these definitions for the PostgreSQL driver to avoid changes for users
// upgrading from old SFTPGo versions
initialSQL = strings.ReplaceAll(initialSQL, "DEFERRABLE INITIALLY DEFERRED", "")
}
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{initialSQL}, 19, true) return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{initialSQL}, 19, true)
} }
@ -520,6 +616,8 @@ func (p *PGSQLProvider) migrateDatabase() error { //nolint:dupl
providerLog(logger.LevelError, "%v", err) providerLog(logger.LevelError, "%v", err)
logger.ErrorToConsole("%v", err) logger.ErrorToConsole("%v", err)
return err return err
case version == 19:
return updatePgSQLDatabaseFromV19(p.dbHandle)
default: default:
if version > sqlDatabaseVersion { if version > sqlDatabaseVersion {
providerLog(logger.LevelError, "database version %v is newer than the supported one: %v", version, providerLog(logger.LevelError, "database version %v is newer than the supported one: %v", version,
@ -542,6 +640,8 @@ func (p *PGSQLProvider) revertDatabase(targetVersion int) error {
} }
switch dbVersion.Version { switch dbVersion.Version {
case 20:
return downgradePgSQLDatabaseFromV20(p.dbHandle)
default: default:
return fmt.Errorf("database version not handled: %v", dbVersion.Version) return fmt.Errorf("database version not handled: %v", dbVersion.Version)
} }
@ -551,3 +651,34 @@ func (p *PGSQLProvider) resetDatabase() error {
sql := sqlReplaceAll(pgsqlResetSQL) sql := sqlReplaceAll(pgsqlResetSQL)
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{sql}, 0, false) return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{sql}, 0, false)
} }
func updatePgSQLDatabaseFromV19(dbHandle *sql.DB) error {
return updatePgSQLDatabaseFrom19To20(dbHandle)
}
func downgradePgSQLDatabaseFromV20(dbHandle *sql.DB) error {
return downgradePgSQLDatabaseFrom20To19(dbHandle)
}
func updatePgSQLDatabaseFrom19To20(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 19 -> 20")
providerLog(logger.LevelInfo, "updating database version: 19 -> 20")
sql := strings.ReplaceAll(pgsqlV20SQL, "{{events_actions}}", sqlTableEventsActions)
sql = strings.ReplaceAll(sql, "{{events_rules}}", sqlTableEventsRules)
sql = strings.ReplaceAll(sql, "{{rules_actions_mapping}}", sqlTableRulesActionsMapping)
sql = strings.ReplaceAll(sql, "{{tasks}}", sqlTableTasks)
sql = strings.ReplaceAll(sql, "{{users}}", sqlTableUsers)
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 20, true)
}
func downgradePgSQLDatabaseFrom20To19(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 20 -> 19")
providerLog(logger.LevelInfo, "downgrading database version: 20 -> 19")
sql := strings.ReplaceAll(pgsqlV20DownSQL, "{{events_actions}}", sqlTableEventsActions)
sql = strings.ReplaceAll(sql, "{{events_rules}}", sqlTableEventsRules)
sql = strings.ReplaceAll(sql, "{{rules_actions_mapping}}", sqlTableRulesActionsMapping)
sql = strings.ReplaceAll(sql, "{{users}}", sqlTableUsers)
sql = strings.ReplaceAll(sql, "{{tasks}}", sqlTableTasks)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 19, false)
}

127
dataprovider/quota.go Normal file
View file

@ -0,0 +1,127 @@
package dataprovider
import (
"sync"
"time"
"github.com/drakkan/sftpgo/v2/util"
)
var (
// QuotaScans is the list of active quota scans
QuotaScans ActiveScans
)
// ActiveQuotaScan defines an active quota scan for a user home dir
type ActiveQuotaScan struct {
// Username to which the quota scan refers
Username string `json:"username"`
// quota scan start time as unix timestamp in milliseconds
StartTime int64 `json:"start_time"`
}
// ActiveVirtualFolderQuotaScan defines an active quota scan for a virtual folder
type ActiveVirtualFolderQuotaScan struct {
// folder name to which the quota scan refers
Name string `json:"name"`
// quota scan start time as unix timestamp in milliseconds
StartTime int64 `json:"start_time"`
}
// ActiveScans holds the active quota scans
type ActiveScans struct {
sync.RWMutex
UserScans []ActiveQuotaScan
FolderScans []ActiveVirtualFolderQuotaScan
}
// GetUsersQuotaScans returns the active quota scans for users home directories
func (s *ActiveScans) GetUsersQuotaScans() []ActiveQuotaScan {
s.RLock()
defer s.RUnlock()
scans := make([]ActiveQuotaScan, len(s.UserScans))
copy(scans, s.UserScans)
return scans
}
// AddUserQuotaScan adds a user to the ones with active quota scans.
// Returns false if the user has a quota scan already running
func (s *ActiveScans) AddUserQuotaScan(username string) bool {
s.Lock()
defer s.Unlock()
for _, scan := range s.UserScans {
if scan.Username == username {
return false
}
}
s.UserScans = append(s.UserScans, ActiveQuotaScan{
Username: username,
StartTime: util.GetTimeAsMsSinceEpoch(time.Now()),
})
return true
}
// RemoveUserQuotaScan removes a user from the ones with active quota scans.
// Returns false if the user has no active quota scans
func (s *ActiveScans) RemoveUserQuotaScan(username string) bool {
s.Lock()
defer s.Unlock()
for idx, scan := range s.UserScans {
if scan.Username == username {
lastIdx := len(s.UserScans) - 1
s.UserScans[idx] = s.UserScans[lastIdx]
s.UserScans = s.UserScans[:lastIdx]
return true
}
}
return false
}
// GetVFoldersQuotaScans returns the active quota scans for virtual folders
func (s *ActiveScans) GetVFoldersQuotaScans() []ActiveVirtualFolderQuotaScan {
s.RLock()
defer s.RUnlock()
scans := make([]ActiveVirtualFolderQuotaScan, len(s.FolderScans))
copy(scans, s.FolderScans)
return scans
}
// AddVFolderQuotaScan adds a virtual folder to the ones with active quota scans.
// Returns false if the folder has a quota scan already running
func (s *ActiveScans) AddVFolderQuotaScan(folderName string) bool {
s.Lock()
defer s.Unlock()
for _, scan := range s.FolderScans {
if scan.Name == folderName {
return false
}
}
s.FolderScans = append(s.FolderScans, ActiveVirtualFolderQuotaScan{
Name: folderName,
StartTime: util.GetTimeAsMsSinceEpoch(time.Now()),
})
return true
}
// RemoveVFolderQuotaScan removes a folder from the ones with active quota scans.
// Returns false if the folder has no active quota scans
func (s *ActiveScans) RemoveVFolderQuotaScan(folderName string) bool {
s.Lock()
defer s.Unlock()
for idx, scan := range s.FolderScans {
if scan.Name == folderName {
lastIdx := len(s.FolderScans) - 1
s.FolderScans[idx] = s.FolderScans[lastIdx]
s.FolderScans = s.FolderScans[:lastIdx]
return true
}
}
return false
}

View file

@ -14,10 +14,11 @@ import (
var ( var (
scheduler *cron.Cron scheduler *cron.Cron
lastCachesUpdate int64 lastUserCacheUpdate int64
// used for bolt and memory providers, so we avoid iterating all users // used for bolt and memory providers, so we avoid iterating all users/rules
// to find recently modified ones // to find recently modified ones
lastUserUpdate int64 lastUserUpdate int64
lastRuleUpdate int64
) )
func stopScheduler() { func stopScheduler() {
@ -30,30 +31,22 @@ func stopScheduler() {
func startScheduler() error { func startScheduler() error {
stopScheduler() stopScheduler()
scheduler = cron.New() scheduler = cron.New(cron.WithLocation(time.UTC))
_, err := scheduler.AddFunc("@every 30s", checkDataprovider) _, err := scheduler.AddFunc("@every 60s", checkDataprovider)
if err != nil { if err != nil {
return fmt.Errorf("unable to schedule dataprovider availability check: %w", err) return fmt.Errorf("unable to schedule dataprovider availability check: %w", err)
} }
if config.AutoBackup.Enabled {
spec := fmt.Sprintf("0 %v * * %v", config.AutoBackup.Hour, config.AutoBackup.DayOfWeek)
_, err = scheduler.AddFunc(spec, config.doBackup)
if err != nil {
return fmt.Errorf("unable to schedule auto backup: %w", err)
}
}
err = addScheduledCacheUpdates() err = addScheduledCacheUpdates()
if err != nil { if err != nil {
return err return err
} }
EventManager.loadRules()
scheduler.Start() scheduler.Start()
return nil return nil
} }
func addScheduledCacheUpdates() error { func addScheduledCacheUpdates() error {
lastCachesUpdate = util.GetTimeAsMsSinceEpoch(time.Now()) lastUserCacheUpdate = util.GetTimeAsMsSinceEpoch(time.Now())
_, err := scheduler.AddFunc("@every 10m", checkCacheUpdates) _, err := scheduler.AddFunc("@every 10m", checkCacheUpdates)
if err != nil { if err != nil {
return fmt.Errorf("unable to schedule cache updates: %w", err) return fmt.Errorf("unable to schedule cache updates: %w", err)
@ -70,9 +63,9 @@ func checkDataprovider() {
} }
func checkCacheUpdates() { func checkCacheUpdates() {
providerLog(logger.LevelDebug, "start caches check, update time %v", util.GetTimeFromMsecSinceEpoch(lastCachesUpdate)) providerLog(logger.LevelDebug, "start caches check, update time %v", util.GetTimeFromMsecSinceEpoch(lastUserCacheUpdate))
checkTime := util.GetTimeAsMsSinceEpoch(time.Now()) checkTime := util.GetTimeAsMsSinceEpoch(time.Now())
users, err := provider.getRecentlyUpdatedUsers(lastCachesUpdate) users, err := provider.getRecentlyUpdatedUsers(lastUserCacheUpdate)
if err != nil { if err != nil {
providerLog(logger.LevelError, "unable to get recently updated users: %v", err) providerLog(logger.LevelError, "unable to get recently updated users: %v", err)
return return
@ -83,8 +76,9 @@ func checkCacheUpdates() {
cachedPasswords.Remove(user.Username) cachedPasswords.Remove(user.Username)
} }
lastCachesUpdate = checkTime lastUserCacheUpdate = checkTime
providerLog(logger.LevelDebug, "end caches check, new update time %v", util.GetTimeFromMsecSinceEpoch(lastCachesUpdate)) EventManager.loadRules()
providerLog(logger.LevelDebug, "end caches check, new update time %v", util.GetTimeFromMsecSinceEpoch(lastUserCacheUpdate))
} }
func setLastUserUpdate() { func setLastUserUpdate() {
@ -94,3 +88,11 @@ func setLastUserUpdate() {
func getLastUserUpdate() int64 { func getLastUserUpdate() int64 {
return atomic.LoadInt64(&lastUserUpdate) return atomic.LoadInt64(&lastUserUpdate)
} }
func setLastRuleUpdate() {
atomic.StoreInt64(&lastRuleUpdate, util.GetTimeAsMsSinceEpoch(time.Now()))
}
func getLastRuleUpdate() int64 {
return atomic.LoadInt64(&lastRuleUpdate)
}

View file

@ -20,7 +20,7 @@ import (
) )
const ( const (
sqlDatabaseVersion = 19 sqlDatabaseVersion = 20
defaultSQLQueryTimeout = 10 * time.Second defaultSQLQueryTimeout = 10 * time.Second
longSQLQueryTimeout = 60 * time.Second longSQLQueryTimeout = 60 * time.Second
) )
@ -57,6 +57,10 @@ func sqlReplaceAll(sql string) string {
sql = strings.ReplaceAll(sql, "{{defender_hosts}}", sqlTableDefenderHosts) sql = strings.ReplaceAll(sql, "{{defender_hosts}}", sqlTableDefenderHosts)
sql = strings.ReplaceAll(sql, "{{active_transfers}}", sqlTableActiveTransfers) sql = strings.ReplaceAll(sql, "{{active_transfers}}", sqlTableActiveTransfers)
sql = strings.ReplaceAll(sql, "{{shared_sessions}}", sqlTableSharedSessions) sql = strings.ReplaceAll(sql, "{{shared_sessions}}", sqlTableSharedSessions)
sql = strings.ReplaceAll(sql, "{{events_actions}}", sqlTableEventsActions)
sql = strings.ReplaceAll(sql, "{{events_rules}}", sqlTableEventsRules)
sql = strings.ReplaceAll(sql, "{{rules_actions_mapping}}", sqlTableRulesActionsMapping)
sql = strings.ReplaceAll(sql, "{{tasks}}", sqlTableTasks)
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix) sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
return sql return sql
} }
@ -655,17 +659,16 @@ func sqlCommonAddGroup(group *Group, dbHandle *sql.DB) error {
if err := group.validate(); err != nil { if err := group.validate(); err != nil {
return err return err
} }
settings, err := json.Marshal(group.UserSettings)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout) ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel() defer cancel()
return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error { return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {
q := getAddGroupQuery() q := getAddGroupQuery()
_, err := tx.ExecContext(ctx, q, group.Name, group.Description, util.GetTimeAsMsSinceEpoch(time.Now()),
settings, err := json.Marshal(group.UserSettings)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, q, group.Name, group.Description, util.GetTimeAsMsSinceEpoch(time.Now()),
util.GetTimeAsMsSinceEpoch(time.Now()), string(settings)) util.GetTimeAsMsSinceEpoch(time.Now()), string(settings))
if err != nil { if err != nil {
return err return err
@ -678,17 +681,17 @@ func sqlCommonUpdateGroup(group *Group, dbHandle *sql.DB) error {
if err := group.validate(); err != nil { if err := group.validate(); err != nil {
return err return err
} }
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {
q := getUpdateGroupQuery()
settings, err := json.Marshal(group.UserSettings) settings, err := json.Marshal(group.UserSettings)
if err != nil { if err != nil {
return err return err
} }
_, err = tx.ExecContext(ctx, q, group.Description, settings, util.GetTimeAsMsSinceEpoch(time.Now()), group.Name) ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {
q := getUpdateGroupQuery()
_, err := tx.ExecContext(ctx, q, group.Description, settings, util.GetTimeAsMsSinceEpoch(time.Now()), group.Name)
if err != nil { if err != nil {
return err return err
} }
@ -898,11 +901,7 @@ func sqlCommonAddUser(user *User, dbHandle *sql.DB) error {
if err != nil { if err != nil {
return err return err
} }
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {
q := getAddUserQuery()
permissions, err := user.GetPermissionsAsJSON() permissions, err := user.GetPermissionsAsJSON()
if err != nil { if err != nil {
return err return err
@ -919,7 +918,12 @@ func sqlCommonAddUser(user *User, dbHandle *sql.DB) error {
if err != nil { if err != nil {
return err return err
} }
_, err = tx.ExecContext(ctx, q, user.Username, user.Password, string(publicKeys), user.HomeDir, user.UID, user.GID, ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {
q := getAddUserQuery()
_, err := tx.ExecContext(ctx, q, user.Username, user.Password, string(publicKeys), user.HomeDir, user.UID, user.GID,
user.MaxSessions, user.QuotaSize, user.QuotaFiles, string(permissions), user.UploadBandwidth, user.MaxSessions, user.QuotaSize, user.QuotaFiles, string(permissions), user.UploadBandwidth,
user.DownloadBandwidth, user.Status, user.ExpirationDate, string(filters), string(fsConfig), user.AdditionalInfo, user.DownloadBandwidth, user.Status, user.ExpirationDate, string(filters), string(fsConfig), user.AdditionalInfo,
user.Description, user.Email, util.GetTimeAsMsSinceEpoch(time.Now()), util.GetTimeAsMsSinceEpoch(time.Now()), user.Description, user.Email, util.GetTimeAsMsSinceEpoch(time.Now()), util.GetTimeAsMsSinceEpoch(time.Now()),
@ -948,11 +952,7 @@ func sqlCommonUpdateUser(user *User, dbHandle *sql.DB) error {
if err != nil { if err != nil {
return err return err
} }
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {
q := getUpdateUserQuery()
permissions, err := user.GetPermissionsAsJSON() permissions, err := user.GetPermissionsAsJSON()
if err != nil { if err != nil {
return err return err
@ -969,7 +969,12 @@ func sqlCommonUpdateUser(user *User, dbHandle *sql.DB) error {
if err != nil { if err != nil {
return err return err
} }
_, err = tx.ExecContext(ctx, q, user.Password, string(publicKeys), user.HomeDir, user.UID, user.GID, user.MaxSessions, ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {
q := getUpdateUserQuery()
_, err := tx.ExecContext(ctx, q, user.Password, string(publicKeys), user.HomeDir, user.UID, user.GID, user.MaxSessions,
user.QuotaSize, user.QuotaFiles, string(permissions), user.UploadBandwidth, user.DownloadBandwidth, user.Status, user.QuotaSize, user.QuotaFiles, string(permissions), user.UploadBandwidth, user.DownloadBandwidth, user.Status,
user.ExpirationDate, string(filters), string(fsConfig), user.AdditionalInfo, user.Description, user.Email, user.ExpirationDate, string(filters), string(fsConfig), user.AdditionalInfo, user.Description, user.Email,
util.GetTimeAsMsSinceEpoch(time.Now()), user.UploadDataTransfer, user.DownloadDataTransfer, user.TotalDataTransfer, util.GetTimeAsMsSinceEpoch(time.Now()), user.UploadDataTransfer, user.DownloadDataTransfer, user.TotalDataTransfer,
@ -1611,6 +1616,55 @@ func getAdminFromDbRow(row sqlScanner) (Admin, error) {
return admin, nil return admin, nil
} }
func getEventActionFromDbRow(row sqlScanner) (BaseEventAction, error) {
var action BaseEventAction
var description sql.NullString
var options []byte
err := row.Scan(&action.ID, &action.Name, &description, &action.Type, &options)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return action, util.NewRecordNotFoundError(err.Error())
}
return action, err
}
if description.Valid {
action.Description = description.String
}
if len(options) > 0 {
err = json.Unmarshal(options, &action.Options)
if err != nil {
return action, err
}
}
return action, nil
}
func getEventRuleFromDbRow(row sqlScanner) (EventRule, error) {
var rule EventRule
var description sql.NullString
var conditions []byte
err := row.Scan(&rule.ID, &rule.Name, &description, &rule.CreatedAt, &rule.UpdatedAt, &rule.Trigger,
&conditions, &rule.DeletedAt)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return rule, util.NewRecordNotFoundError(err.Error())
}
return rule, err
}
if len(conditions) > 0 {
err = json.Unmarshal(conditions, &rule.Conditions)
if err != nil {
return rule, err
}
}
if description.Valid {
rule.Description = description.String
}
return rule, nil
}
func getGroupFromDbRow(row sqlScanner) (Group, error) { func getGroupFromDbRow(row sqlScanner) (Group, error) {
var group Group var group Group
var userSettings, description sql.NullString var userSettings, description sql.NullString
@ -2062,7 +2116,6 @@ func getUsersWithVirtualFolders(ctx context.Context, users []User, dbHandle sqlQ
return users, nil return users, nil
} }
var err error
usersVirtualFolders := make(map[int64][]vfs.VirtualFolder) usersVirtualFolders := make(map[int64][]vfs.VirtualFolder)
q := getRelatedFoldersForUsersQuery(users) q := getRelatedFoldersForUsersQuery(users)
rows, err := dbHandle.QueryContext(ctx, q) rows, err := dbHandle.QueryContext(ctx, q)
@ -2124,7 +2177,6 @@ func getUsersWithGroups(ctx context.Context, users []User, dbHandle sqlQuerier)
if len(users) == 0 { if len(users) == 0 {
return users, nil return users, nil
} }
var err error
usersGroups := make(map[int64][]sdk.GroupMapping) usersGroups := make(map[int64][]sdk.GroupMapping)
q := getRelatedGroupsForUsersQuery(users) q := getRelatedGroupsForUsersQuery(users)
rows, err := dbHandle.QueryContext(ctx, q) rows, err := dbHandle.QueryContext(ctx, q)
@ -2182,8 +2234,6 @@ func getGroupsWithVirtualFolders(ctx context.Context, groups []Group, dbHandle s
if len(groups) == 0 { if len(groups) == 0 {
return groups, nil return groups, nil
} }
var err error
q := getRelatedFoldersForGroupsQuery(groups) q := getRelatedFoldersForGroupsQuery(groups)
rows, err := dbHandle.QueryContext(ctx, q) rows, err := dbHandle.QueryContext(ctx, q)
if err != nil { if err != nil {
@ -2235,8 +2285,6 @@ func getGroupsWithUsers(ctx context.Context, groups []Group, dbHandle sqlQuerier
if len(groups) == 0 { if len(groups) == 0 {
return groups, nil return groups, nil
} }
var err error
q := getRelatedUsersForGroupsQuery(groups) q := getRelatedUsersForGroupsQuery(groups)
rows, err := dbHandle.QueryContext(ctx, q) rows, err := dbHandle.QueryContext(ctx, q)
if err != nil { if err != nil {
@ -2272,7 +2320,6 @@ func getVirtualFoldersWithGroups(folders []vfs.BaseVirtualFolder, dbHandle sqlQu
if len(folders) == 0 { if len(folders) == 0 {
return folders, nil return folders, nil
} }
var err error
vFoldersGroups := make(map[int64][]string) vFoldersGroups := make(map[int64][]string)
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout) ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel() defer cancel()
@ -2310,7 +2357,6 @@ func getVirtualFoldersWithUsers(folders []vfs.BaseVirtualFolder, dbHandle sqlQue
if len(folders) == 0 { if len(folders) == 0 {
return folders, nil return folders, nil
} }
var err error
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout) ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel() defer cancel()
@ -2509,6 +2555,492 @@ func sqlCommonCleanupSessions(sessionType SessionType, before int64, dbHandle *s
return err return err
} }
func getActionsWithRuleNames(ctx context.Context, actions []BaseEventAction, dbHandle sqlQuerier,
) ([]BaseEventAction, error) {
if len(actions) == 0 {
return actions, nil
}
q := getRelatedRulesForActionsQuery(actions)
rows, err := dbHandle.QueryContext(ctx, q)
if err != nil {
return nil, err
}
defer rows.Close()
actionsRules := make(map[int64][]string)
for rows.Next() {
var name string
var actionID int64
if err = rows.Scan(&actionID, &name); err != nil {
return nil, err
}
actionsRules[actionID] = append(actionsRules[actionID], name)
}
err = rows.Err()
if err != nil {
return nil, err
}
if len(actionsRules) == 0 {
return actions, nil
}
for idx := range actions {
ref := &actions[idx]
ref.Rules = actionsRules[ref.ID]
}
return actions, nil
}
func getRulesWithActions(ctx context.Context, rules []EventRule, dbHandle sqlQuerier) ([]EventRule, error) {
if len(rules) == 0 {
return rules, nil
}
rulesActions := make(map[int64][]EventAction)
q := getRelatedActionsForRulesQuery(rules)
rows, err := dbHandle.QueryContext(ctx, q)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var action EventAction
var ruleID int64
var description sql.NullString
var baseOptions, options []byte
err = rows.Scan(&action.ID, &action.Name, &description, &action.Type, &baseOptions, &options,
&action.Order, &ruleID)
if err != nil {
return rules, err
}
if len(baseOptions) > 0 {
err = json.Unmarshal(baseOptions, &action.BaseEventAction.Options)
if err != nil {
return rules, err
}
}
if len(options) > 0 {
err = json.Unmarshal(options, &action.Options)
if err != nil {
return rules, err
}
}
action.BaseEventAction.Options.SetEmptySecretsIfNil()
rulesActions[ruleID] = append(rulesActions[ruleID], action)
}
err = rows.Err()
if err != nil {
return rules, err
}
if len(rulesActions) == 0 {
return rules, nil
}
for idx := range rules {
ref := &rules[idx]
ref.Actions = rulesActions[ref.ID]
}
return rules, nil
}
func generateEventRuleActionsMapping(ctx context.Context, rule *EventRule, dbHandle sqlQuerier) error {
q := getClearRuleActionMappingQuery()
_, err := dbHandle.ExecContext(ctx, q, rule.Name)
if err != nil {
return err
}
for _, action := range rule.Actions {
options, err := json.Marshal(action.Options)
if err != nil {
return err
}
q = getAddRuleActionMappingQuery()
_, err = dbHandle.ExecContext(ctx, q, rule.Name, action.Name, action.Order, string(options))
if err != nil {
return err
}
}
return nil
}
func sqlCommonGetEventActionByName(name string, dbHandle sqlQuerier) (BaseEventAction, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getEventActionByNameQuery()
row := dbHandle.QueryRowContext(ctx, q, name)
action, err := getEventActionFromDbRow(row)
if err != nil {
return action, err
}
actions, err := getActionsWithRuleNames(ctx, []BaseEventAction{action}, dbHandle)
if err != nil {
return action, err
}
if len(actions) != 1 {
return action, fmt.Errorf("unable to associate rules with action %q", name)
}
return actions[0], nil
}
func sqlCommonDumpEventActions(dbHandle sqlQuerier) ([]BaseEventAction, error) {
actions := make([]BaseEventAction, 0, 10)
ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)
defer cancel()
q := getDumpEventActionsQuery()
rows, err := dbHandle.QueryContext(ctx, q)
if err != nil {
return actions, err
}
defer rows.Close()
for rows.Next() {
action, err := getEventActionFromDbRow(rows)
if err != nil {
return actions, err
}
actions = append(actions, action)
}
return actions, rows.Err()
}
func sqlCommonGetEventActions(limit int, offset int, order string, minimal bool,
dbHandle sqlQuerier,
) ([]BaseEventAction, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getEventsActionsQuery(order, minimal)
actions := make([]BaseEventAction, 0, limit)
rows, err := dbHandle.QueryContext(ctx, q, limit, offset)
if err != nil {
return actions, err
}
defer rows.Close()
for rows.Next() {
var action BaseEventAction
if minimal {
err = rows.Scan(&action.ID, &action.Name)
} else {
action, err = getEventActionFromDbRow(rows)
}
if err != nil {
return actions, err
}
actions = append(actions, action)
}
err = rows.Err()
if err != nil {
return nil, err
}
if minimal {
return actions, nil
}
actions, err = getActionsWithRuleNames(ctx, actions, dbHandle)
if err != nil {
return nil, err
}
for idx := range actions {
actions[idx].PrepareForRendering()
}
return actions, nil
}
func sqlCommonAddEventAction(action *BaseEventAction, dbHandle *sql.DB) error {
if err := action.validate(); err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getAddEventActionQuery()
options, err := json.Marshal(action.Options)
if err != nil {
return err
}
_, err = dbHandle.ExecContext(ctx, q, action.Name, action.Description, action.Type, string(options))
return err
}
func sqlCommonUpdateEventAction(action *BaseEventAction, dbHandle *sql.DB) error {
if err := action.validate(); err != nil {
return err
}
options, err := json.Marshal(action.Options)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {
q := getUpdateEventActionQuery()
_, err = tx.ExecContext(ctx, q, action.Description, action.Type, string(options), action.Name)
if err != nil {
return err
}
q = getUpdateRulesTimestampQuery()
_, err = tx.ExecContext(ctx, q, util.GetTimeAsMsSinceEpoch(time.Now()), action.ID)
return err
})
}
func sqlCommonDeleteEventAction(action BaseEventAction, dbHandle *sql.DB) error {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getDeleteEventActionQuery()
res, err := dbHandle.ExecContext(ctx, q, action.Name)
if err != nil {
return err
}
return sqlCommonRequireRowAffected(res)
}
func sqlCommonGetEventRuleByName(name string, dbHandle sqlQuerier) (EventRule, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getEventRulesByNameQuery()
row := dbHandle.QueryRowContext(ctx, q, name)
rule, err := getEventRuleFromDbRow(row)
if err != nil {
return rule, err
}
rules, err := getRulesWithActions(ctx, []EventRule{rule}, dbHandle)
if err != nil {
return rule, err
}
if len(rules) != 1 {
return rule, fmt.Errorf("unable to associate rule %q with actions", name)
}
return rules[0], nil
}
func sqlCommonDumpEventRules(dbHandle sqlQuerier) ([]EventRule, error) {
rules := make([]EventRule, 0, 10)
ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)
defer cancel()
q := getDumpEventRulesQuery()
rows, err := dbHandle.QueryContext(ctx, q)
if err != nil {
return rules, err
}
defer rows.Close()
for rows.Next() {
rule, err := getEventRuleFromDbRow(rows)
if err != nil {
return rules, err
}
rules = append(rules, rule)
}
err = rows.Err()
if err != nil {
return rules, err
}
return getRulesWithActions(ctx, rules, dbHandle)
}
func sqlCommonGetRecentlyUpdatedRules(after int64, dbHandle sqlQuerier) ([]EventRule, error) {
rules := make([]EventRule, 0, 10)
ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)
defer cancel()
q := getRecentlyUpdatedRulesQuery()
rows, err := dbHandle.QueryContext(ctx, q, after)
if err != nil {
return rules, err
}
defer rows.Close()
for rows.Next() {
rule, err := getEventRuleFromDbRow(rows)
if err != nil {
return rules, err
}
rules = append(rules, rule)
}
err = rows.Err()
if err != nil {
return rules, err
}
return getRulesWithActions(ctx, rules, dbHandle)
}
func sqlCommonGetEventRules(limit int, offset int, order string, dbHandle sqlQuerier) ([]EventRule, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getEventRulesQuery(order)
rules := make([]EventRule, 0, limit)
rows, err := dbHandle.QueryContext(ctx, q, limit, offset)
if err != nil {
return rules, err
}
defer rows.Close()
for rows.Next() {
rule, err := getEventRuleFromDbRow(rows)
if err != nil {
return rules, err
}
rules = append(rules, rule)
}
err = rows.Err()
if err != nil {
return rules, err
}
rules, err = getRulesWithActions(ctx, rules, dbHandle)
if err != nil {
return rules, err
}
for idx := range rules {
rules[idx].PrepareForRendering()
}
return rules, nil
}
func sqlCommonAddEventRule(rule *EventRule, dbHandle *sql.DB) error {
if err := rule.validate(); err != nil {
return err
}
conditions, err := json.Marshal(rule.Conditions)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {
q := getAddEventRuleQuery()
_, err := tx.ExecContext(ctx, q, rule.Name, rule.Description, util.GetTimeAsMsSinceEpoch(time.Now()),
util.GetTimeAsMsSinceEpoch(time.Now()), rule.Trigger, string(conditions))
if err != nil {
return err
}
return generateEventRuleActionsMapping(ctx, rule, tx)
})
}
func sqlCommonUpdateEventRule(rule *EventRule, dbHandle *sql.DB) error {
if err := rule.validate(); err != nil {
return err
}
conditions, err := json.Marshal(rule.Conditions)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {
q := getUpdateEventRuleQuery()
_, err := tx.ExecContext(ctx, q, rule.Description, util.GetTimeAsMsSinceEpoch(time.Now()),
rule.Trigger, string(conditions), rule.Name)
if err != nil {
return err
}
return generateEventRuleActionsMapping(ctx, rule, tx)
})
}
func sqlCommonDeleteEventRule(rule EventRule, softDelete bool, dbHandle *sql.DB) error {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {
if softDelete {
q := getClearRuleActionMappingQuery()
_, err := tx.ExecContext(ctx, q, rule.Name)
if err != nil {
return err
}
}
q := getDeleteEventRuleQuery(softDelete)
if softDelete {
ts := util.GetTimeAsMsSinceEpoch(time.Now())
res, err := tx.ExecContext(ctx, q, ts, ts, rule.Name)
if err != nil {
return err
}
return sqlCommonRequireRowAffected(res)
}
res, err := tx.ExecContext(ctx, q, rule.Name)
if err != nil {
return err
}
if err = sqlCommonRequireRowAffected(res); err != nil {
return err
}
return sqlCommonDeleteTask(rule.Name, tx)
})
}
func sqlCommonGetTaskByName(name string, dbHandle sqlQuerier) (Task, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
task := Task{
Name: name,
}
q := getTaskByNameQuery()
row := dbHandle.QueryRowContext(ctx, q, name)
err := row.Scan(&task.UpdateAt, &task.Version)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return task, util.NewRecordNotFoundError(err.Error())
}
}
return task, err
}
func sqlCommonAddTask(name string, dbHandle *sql.DB) error {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getAddTaskQuery()
_, err := dbHandle.ExecContext(ctx, q, name, util.GetTimeAsMsSinceEpoch(time.Now()))
return err
}
func sqlCommonUpdateTask(name string, version int64, dbHandle *sql.DB) error {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getUpdateTaskQuery()
res, err := dbHandle.ExecContext(ctx, q, util.GetTimeAsMsSinceEpoch(time.Now()), name, version)
if err != nil {
return err
}
return sqlCommonRequireRowAffected(res)
}
func sqlCommonUpdateTaskTimestamp(name string, dbHandle *sql.DB) error {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getUpdateTaskTimestampQuery()
res, err := dbHandle.ExecContext(ctx, q, util.GetTimeAsMsSinceEpoch(time.Now()), name)
if err != nil {
return err
}
return sqlCommonRequireRowAffected(res)
}
func sqlCommonDeleteTask(name string, dbHandle sqlQuerier) error {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getDeleteTaskQuery()
_, err := dbHandle.ExecContext(ctx, q, name)
return err
}
func sqlCommonGetDatabaseVersion(dbHandle sqlQuerier, showInitWarn bool) (schemaVersion, error) { func sqlCommonGetDatabaseVersion(dbHandle sqlQuerier, showInitWarn bool) (schemaVersion, error) {
var result schemaVersion var result schemaVersion
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout) ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
@ -2594,7 +3126,7 @@ func sqlAcquireLock(dbHandle *sql.DB) error {
return errors.New("unable to get lock: null value returned") return errors.New("unable to get lock: null value returned")
} }
if lockResult.Int64 != 1 { if lockResult.Int64 != 1 {
return fmt.Errorf("unable to get lock, result: %v", lockResult.Int64) return fmt.Errorf("unable to get lock, result: %d", lockResult.Int64)
} }
providerLog(logger.LevelInfo, "acquired database lock") providerLog(logger.LevelInfo, "acquired database lock")
} }

View file

@ -10,6 +10,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"path/filepath" "path/filepath"
"strings"
"time" "time"
// we import go-sqlite3 here to be able to disable SQLite support using a build tag // we import go-sqlite3 here to be able to disable SQLite support using a build tag
@ -36,6 +37,10 @@ DROP TABLE IF EXISTS "{{defender_events}}";
DROP TABLE IF EXISTS "{{defender_hosts}}"; DROP TABLE IF EXISTS "{{defender_hosts}}";
DROP TABLE IF EXISTS "{{active_transfers}}"; DROP TABLE IF EXISTS "{{active_transfers}}";
DROP TABLE IF EXISTS "{{shared_sessions}}"; DROP TABLE IF EXISTS "{{shared_sessions}}";
DROP TABLE IF EXISTS "{{rules_actions_mapping}}";
DROP TABLE IF EXISTS "{{events_rules}}";
DROP TABLE IF EXISTS "{{events_actions}}";
DROP TABLE IF EXISTS "{{tasks}}";
DROP TABLE IF EXISTS "{{schema_version}}"; DROP TABLE IF EXISTS "{{schema_version}}";
` `
sqliteInitialSQL = `CREATE TABLE "{{schema_version}}" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "version" integer NOT NULL); sqliteInitialSQL = `CREATE TABLE "{{schema_version}}" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "version" integer NOT NULL);
@ -115,6 +120,33 @@ CREATE INDEX "{{prefix}}active_transfers_updated_at_idx" ON "{{active_transfers}
CREATE INDEX "{{prefix}}shared_sessions_type_idx" ON "{{shared_sessions}}" ("type"); CREATE INDEX "{{prefix}}shared_sessions_type_idx" ON "{{shared_sessions}}" ("type");
CREATE INDEX "{{prefix}}shared_sessions_timestamp_idx" ON "{{shared_sessions}}" ("timestamp"); CREATE INDEX "{{prefix}}shared_sessions_timestamp_idx" ON "{{shared_sessions}}" ("timestamp");
INSERT INTO {{schema_version}} (version) VALUES (19); INSERT INTO {{schema_version}} (version) VALUES (19);
`
sqliteV20SQL = `CREATE TABLE "{{events_rules}}" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" varchar(255) NOT NULL UNIQUE, "description" varchar(512) NULL, "created_at" bigint NOT NULL,
"updated_at" bigint NOT NULL, "trigger" integer NOT NULL, "conditions" text NOT NULL, "deleted_at" bigint NOT NULL);
CREATE TABLE "{{events_actions}}" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(255) NOT NULL UNIQUE,
"description" varchar(512) NULL, "type" integer NOT NULL, "options" text NOT NULL);
CREATE TABLE "{{rules_actions_mapping}}" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"rule_id" integer NOT NULL REFERENCES "{{events_rules}}" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
"action_id" integer NOT NULL REFERENCES "{{events_actions}}" ("id") ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED,
"order" integer NOT NULL, "options" text NOT NULL,
CONSTRAINT "{{prefix}}unique_rule_action_mapping" UNIQUE ("rule_id", "action_id"));
CREATE TABLE "{{tasks}}" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(255) NOT NULL UNIQUE,
"updated_at" bigint NOT NULL, "version" bigint NOT NULL);
ALTER TABLE "{{users}}" ADD COLUMN "deleted_at" bigint DEFAULT 0 NOT NULL;
CREATE INDEX "{{prefix}}events_rules_updated_at_idx" ON "{{events_rules}}" ("updated_at");
CREATE INDEX "{{prefix}}events_rules_deleted_at_idx" ON "{{events_rules}}" ("deleted_at");
CREATE INDEX "{{prefix}}events_rules_trigger_idx" ON "{{events_rules}}" ("trigger");
CREATE INDEX "{{prefix}}rules_actions_mapping_rule_id_idx" ON "{{rules_actions_mapping}}" ("rule_id");
CREATE INDEX "{{prefix}}rules_actions_mapping_action_id_idx" ON "{{rules_actions_mapping}}" ("action_id");
CREATE INDEX "{{prefix}}rules_actions_mapping_order_idx" ON "{{rules_actions_mapping}}" ("order");
CREATE INDEX "{{prefix}}users_deleted_at_idx" ON "{{users}}" ("deleted_at");
`
sqliteV20DownSQL = `DROP TABLE "{{rules_actions_mapping}}";
DROP TABLE "{{events_rules}}";
DROP TABLE "{{events_actions}}";
DROP TABLE "{{tasks}}";
ALTER TABLE "{{users}}" DROP COLUMN "deleted_at";
` `
) )
@ -449,6 +481,74 @@ func (p *SQLiteProvider) cleanupSharedSessions(sessionType SessionType, before i
return sqlCommonCleanupSessions(sessionType, before, p.dbHandle) return sqlCommonCleanupSessions(sessionType, before, p.dbHandle)
} }
func (p *SQLiteProvider) getEventActions(limit, offset int, order string, minimal bool) ([]BaseEventAction, error) {
return sqlCommonGetEventActions(limit, offset, order, minimal, p.dbHandle)
}
func (p *SQLiteProvider) dumpEventActions() ([]BaseEventAction, error) {
return sqlCommonDumpEventActions(p.dbHandle)
}
func (p *SQLiteProvider) eventActionExists(name string) (BaseEventAction, error) {
return sqlCommonGetEventActionByName(name, p.dbHandle)
}
func (p *SQLiteProvider) addEventAction(action *BaseEventAction) error {
return sqlCommonAddEventAction(action, p.dbHandle)
}
func (p *SQLiteProvider) updateEventAction(action *BaseEventAction) error {
return sqlCommonUpdateEventAction(action, p.dbHandle)
}
func (p *SQLiteProvider) deleteEventAction(action BaseEventAction) error {
return sqlCommonDeleteEventAction(action, p.dbHandle)
}
func (p *SQLiteProvider) getEventRules(limit, offset int, order string) ([]EventRule, error) {
return sqlCommonGetEventRules(limit, offset, order, p.dbHandle)
}
func (p *SQLiteProvider) dumpEventRules() ([]EventRule, error) {
return sqlCommonDumpEventRules(p.dbHandle)
}
func (p *SQLiteProvider) getRecentlyUpdatedRules(after int64) ([]EventRule, error) {
return sqlCommonGetRecentlyUpdatedRules(after, p.dbHandle)
}
func (p *SQLiteProvider) eventRuleExists(name string) (EventRule, error) {
return sqlCommonGetEventRuleByName(name, p.dbHandle)
}
func (p *SQLiteProvider) addEventRule(rule *EventRule) error {
return sqlCommonAddEventRule(rule, p.dbHandle)
}
func (p *SQLiteProvider) updateEventRule(rule *EventRule) error {
return sqlCommonUpdateEventRule(rule, p.dbHandle)
}
func (p *SQLiteProvider) deleteEventRule(rule EventRule, softDelete bool) error {
return sqlCommonDeleteEventRule(rule, softDelete, p.dbHandle)
}
func (p *SQLiteProvider) getTaskByName(name string) (Task, error) {
return sqlCommonGetTaskByName(name, p.dbHandle)
}
func (p *SQLiteProvider) addTask(name string) error {
return sqlCommonAddTask(name, p.dbHandle)
}
func (p *SQLiteProvider) updateTask(name string, version int64) error {
return sqlCommonUpdateTask(name, version, p.dbHandle)
}
func (p *SQLiteProvider) updateTaskTimestamp(name string) error {
return sqlCommonUpdateTaskTimestamp(name, p.dbHandle)
}
func (p *SQLiteProvider) close() error { func (p *SQLiteProvider) close() error {
return p.dbHandle.Close() return p.dbHandle.Close()
} }
@ -488,6 +588,8 @@ func (p *SQLiteProvider) migrateDatabase() error { //nolint:dupl
providerLog(logger.LevelError, "%v", err) providerLog(logger.LevelError, "%v", err)
logger.ErrorToConsole("%v", err) logger.ErrorToConsole("%v", err)
return err return err
case version == 19:
return updateSQLiteDatabaseFromV19(p.dbHandle)
default: default:
if version > sqlDatabaseVersion { if version > sqlDatabaseVersion {
providerLog(logger.LevelError, "database version %v is newer than the supported one: %v", version, providerLog(logger.LevelError, "database version %v is newer than the supported one: %v", version,
@ -510,6 +612,8 @@ func (p *SQLiteProvider) revertDatabase(targetVersion int) error {
} }
switch dbVersion.Version { switch dbVersion.Version {
case 20:
return downgradeSQLiteDatabaseFromV20(p.dbHandle)
default: default:
return fmt.Errorf("database version not handled: %v", dbVersion.Version) return fmt.Errorf("database version not handled: %v", dbVersion.Version)
} }
@ -520,6 +624,37 @@ func (p *SQLiteProvider) resetDatabase() error {
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{sql}, 0, false) return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{sql}, 0, false)
} }
func updateSQLiteDatabaseFromV19(dbHandle *sql.DB) error {
return updateSQLiteDatabaseFrom19To20(dbHandle)
}
func downgradeSQLiteDatabaseFromV20(dbHandle *sql.DB) error {
return downgradeSQLiteDatabaseFrom20To19(dbHandle)
}
func updateSQLiteDatabaseFrom19To20(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 19 -> 20")
providerLog(logger.LevelInfo, "updating database version: 19 -> 20")
sql := strings.ReplaceAll(sqliteV20SQL, "{{events_actions}}", sqlTableEventsActions)
sql = strings.ReplaceAll(sql, "{{events_rules}}", sqlTableEventsRules)
sql = strings.ReplaceAll(sql, "{{rules_actions_mapping}}", sqlTableRulesActionsMapping)
sql = strings.ReplaceAll(sql, "{{tasks}}", sqlTableTasks)
sql = strings.ReplaceAll(sql, "{{users}}", sqlTableUsers)
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 20, true)
}
func downgradeSQLiteDatabaseFrom20To19(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 20 -> 19")
providerLog(logger.LevelInfo, "downgrading database version: 20 -> 19")
sql := strings.ReplaceAll(sqliteV20DownSQL, "{{events_actions}}", sqlTableEventsActions)
sql = strings.ReplaceAll(sql, "{{events_rules}}", sqlTableEventsRules)
sql = strings.ReplaceAll(sql, "{{rules_actions_mapping}}", sqlTableRulesActionsMapping)
sql = strings.ReplaceAll(sql, "{{users}}", sqlTableUsers)
sql = strings.ReplaceAll(sql, "{{tasks}}", sqlTableTasks)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 19, false)
}
/*func setPragmaFK(dbHandle *sql.DB, value string) error { /*func setPragmaFK(dbHandle *sql.DB, value string) error {
ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout) ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)
defer cancel() defer cancel()

View file

@ -19,6 +19,8 @@ const (
selectShareFields = "s.share_id,s.name,s.description,s.scope,s.paths,u.username,s.created_at,s.updated_at,s.last_use_at," + selectShareFields = "s.share_id,s.name,s.description,s.scope,s.paths,u.username,s.created_at,s.updated_at,s.last_use_at," +
"s.expires_at,s.password,s.max_tokens,s.used_tokens,s.allow_from" "s.expires_at,s.password,s.max_tokens,s.used_tokens,s.allow_from"
selectGroupFields = "id,name,description,created_at,updated_at,user_settings" selectGroupFields = "id,name,description,created_at,updated_at,user_settings"
selectEventActionFields = "id,name,description,type,options"
selectMinimalFields = "id,name"
) )
func getSQLPlaceholders() []string { func getSQLPlaceholders() []string {
@ -33,12 +35,20 @@ func getSQLPlaceholders() []string {
return placeholders return placeholders
} }
func getSQLTableGroups() string { func getSQLQuotedName(name string) string {
if config.Driver == MySQLDataProviderName { if config.Driver == MySQLDataProviderName {
return fmt.Sprintf("`%s`", sqlTableGroups) return fmt.Sprintf("`%s`", name)
} }
return sqlTableGroups return fmt.Sprintf(`"%s"`, name)
}
func getSelectEventRuleFields() string {
if config.Driver == MySQLDataProviderName {
return "id,name,description,created_at,updated_at,`trigger`,conditions,deleted_at"
}
return `id,name,description,created_at,updated_at,"trigger",conditions,deleted_at`
} }
func getAddSessionQuery() string { func getAddSessionQuery() string {
@ -147,18 +157,19 @@ func getDefenderEventsCleanupQuery() string {
} }
func getGroupByNameQuery() string { func getGroupByNameQuery() string {
return fmt.Sprintf(`SELECT %s FROM %s WHERE name = %s`, selectGroupFields, getSQLTableGroups(), sqlPlaceholders[0]) return fmt.Sprintf(`SELECT %s FROM %s WHERE name = %s`, selectGroupFields, getSQLQuotedName(sqlTableGroups),
sqlPlaceholders[0])
} }
func getGroupsQuery(order string, minimal bool) string { func getGroupsQuery(order string, minimal bool) string {
var fieldSelection string var fieldSelection string
if minimal { if minimal {
fieldSelection = "id,name" fieldSelection = selectMinimalFields
} else { } else {
fieldSelection = selectGroupFields fieldSelection = selectGroupFields
} }
return fmt.Sprintf(`SELECT %s FROM %s ORDER BY name %s LIMIT %s OFFSET %s`, fieldSelection, getSQLTableGroups(), return fmt.Sprintf(`SELECT %s FROM %s ORDER BY name %s LIMIT %s OFFSET %s`, fieldSelection,
order, sqlPlaceholders[0], sqlPlaceholders[1]) getSQLQuotedName(sqlTableGroups), order, sqlPlaceholders[0], sqlPlaceholders[1])
} }
func getGroupsWithNamesQuery(numArgs int) string { func getGroupsWithNamesQuery(numArgs int) string {
@ -176,7 +187,7 @@ func getGroupsWithNamesQuery(numArgs int) string {
} else { } else {
sb.WriteString("('')") sb.WriteString("('')")
} }
return fmt.Sprintf(`SELECT %s FROM %s WHERE name in %s`, selectGroupFields, getSQLTableGroups(), sb.String()) return fmt.Sprintf(`SELECT %s FROM %s WHERE name in %s`, selectGroupFields, getSQLQuotedName(sqlTableGroups), sb.String())
} }
func getUsersInGroupsQuery(numArgs int) string { func getUsersInGroupsQuery(numArgs int) string {
@ -195,27 +206,27 @@ func getUsersInGroupsQuery(numArgs int) string {
sb.WriteString("('')") sb.WriteString("('')")
} }
return fmt.Sprintf(`SELECT username FROM %s WHERE id IN (SELECT user_id from %s WHERE group_id IN (SELECT id FROM %s WHERE name IN (%s)))`, return fmt.Sprintf(`SELECT username FROM %s WHERE id IN (SELECT user_id from %s WHERE group_id IN (SELECT id FROM %s WHERE name IN (%s)))`,
sqlTableUsers, sqlTableUsersGroupsMapping, getSQLTableGroups(), sb.String()) sqlTableUsers, sqlTableUsersGroupsMapping, getSQLQuotedName(sqlTableGroups), sb.String())
} }
func getDumpGroupsQuery() string { func getDumpGroupsQuery() string {
return fmt.Sprintf(`SELECT %s FROM %s`, selectGroupFields, getSQLTableGroups()) return fmt.Sprintf(`SELECT %s FROM %s`, selectGroupFields, getSQLQuotedName(sqlTableGroups))
} }
func getAddGroupQuery() string { func getAddGroupQuery() string {
return fmt.Sprintf(`INSERT INTO %s (name,description,created_at,updated_at,user_settings) return fmt.Sprintf(`INSERT INTO %s (name,description,created_at,updated_at,user_settings)
VALUES (%s,%s,%s,%s,%s)`, getSQLTableGroups(), sqlPlaceholders[0], sqlPlaceholders[1], VALUES (%s,%s,%s,%s,%s)`, getSQLQuotedName(sqlTableGroups), sqlPlaceholders[0], sqlPlaceholders[1],
sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4]) sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4])
} }
func getUpdateGroupQuery() string { func getUpdateGroupQuery() string {
return fmt.Sprintf(`UPDATE %s SET description=%s,user_settings=%s,updated_at=%s return fmt.Sprintf(`UPDATE %s SET description=%s,user_settings=%s,updated_at=%s
WHERE name = %s`, getSQLTableGroups(), sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], WHERE name = %s`, getSQLQuotedName(sqlTableGroups), sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2],
sqlPlaceholders[3]) sqlPlaceholders[3])
} }
func getDeleteGroupQuery() string { func getDeleteGroupQuery() string {
return fmt.Sprintf(`DELETE FROM %s WHERE name = %s`, getSQLTableGroups(), sqlPlaceholders[0]) return fmt.Sprintf(`DELETE FROM %s WHERE name = %s`, getSQLQuotedName(sqlTableGroups), sqlPlaceholders[0])
} }
func getAdminByUsernameQuery() string { func getAdminByUsernameQuery() string {
@ -457,8 +468,8 @@ func getAddUserQuery() string {
return fmt.Sprintf(`INSERT INTO %s (username,password,public_keys,home_dir,uid,gid,max_sessions,quota_size,quota_files,permissions, return fmt.Sprintf(`INSERT INTO %s (username,password,public_keys,home_dir,uid,gid,max_sessions,quota_size,quota_files,permissions,
used_quota_size,used_quota_files,last_quota_update,upload_bandwidth,download_bandwidth,status,last_login,expiration_date,filters, used_quota_size,used_quota_files,last_quota_update,upload_bandwidth,download_bandwidth,status,last_login,expiration_date,filters,
filesystem,additional_info,description,email,created_at,updated_at,upload_data_transfer,download_data_transfer,total_data_transfer, filesystem,additional_info,description,email,created_at,updated_at,upload_data_transfer,download_data_transfer,total_data_transfer,
used_upload_data_transfer,used_download_data_transfer) used_upload_data_transfer,used_download_data_transfer,deleted_at)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,0,0,0,%s,%s,%s,0,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,0,0)`, VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,0,0,0,%s,%s,%s,0,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,0,0,0)`,
sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4], sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4],
sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7], sqlPlaceholders[8], sqlPlaceholders[9], sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7], sqlPlaceholders[8], sqlPlaceholders[9],
sqlPlaceholders[10], sqlPlaceholders[11], sqlPlaceholders[12], sqlPlaceholders[13], sqlPlaceholders[14], sqlPlaceholders[10], sqlPlaceholders[11], sqlPlaceholders[12], sqlPlaceholders[13], sqlPlaceholders[14],
@ -527,19 +538,20 @@ func getClearUserGroupMappingQuery() string {
func getAddUserGroupMappingQuery() string { func getAddUserGroupMappingQuery() string {
return fmt.Sprintf(`INSERT INTO %s (user_id,group_id,group_type) VALUES ((SELECT id FROM %s WHERE username = %s), return fmt.Sprintf(`INSERT INTO %s (user_id,group_id,group_type) VALUES ((SELECT id FROM %s WHERE username = %s),
(SELECT id FROM %s WHERE name = %s),%s)`, (SELECT id FROM %s WHERE name = %s),%s)`,
sqlTableUsersGroupsMapping, sqlTableUsers, sqlPlaceholders[0], getSQLTableGroups(), sqlPlaceholders[1], sqlPlaceholders[2]) sqlTableUsersGroupsMapping, sqlTableUsers, sqlPlaceholders[0], getSQLQuotedName(sqlTableGroups),
sqlPlaceholders[1], sqlPlaceholders[2])
} }
func getClearGroupFolderMappingQuery() string { func getClearGroupFolderMappingQuery() string {
return fmt.Sprintf(`DELETE FROM %s WHERE group_id = (SELECT id FROM %s WHERE name = %s)`, sqlTableGroupsFoldersMapping, return fmt.Sprintf(`DELETE FROM %s WHERE group_id = (SELECT id FROM %s WHERE name = %s)`, sqlTableGroupsFoldersMapping,
getSQLTableGroups(), sqlPlaceholders[0]) getSQLQuotedName(sqlTableGroups), sqlPlaceholders[0])
} }
func getAddGroupFolderMappingQuery() string { func getAddGroupFolderMappingQuery() string {
return fmt.Sprintf(`INSERT INTO %s (virtual_path,quota_size,quota_files,folder_id,group_id) return fmt.Sprintf(`INSERT INTO %s (virtual_path,quota_size,quota_files,folder_id,group_id)
VALUES (%s,%s,%s,(SELECT id FROM %s WHERE name = %s),(SELECT id FROM %s WHERE name = %s))`, VALUES (%s,%s,%s,(SELECT id FROM %s WHERE name = %s),(SELECT id FROM %s WHERE name = %s))`,
sqlTableGroupsFoldersMapping, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlTableFolders, sqlTableGroupsFoldersMapping, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlTableFolders,
sqlPlaceholders[3], getSQLTableGroups(), sqlPlaceholders[4]) sqlPlaceholders[3], getSQLQuotedName(sqlTableGroups), sqlPlaceholders[4])
} }
func getClearUserFolderMappingQuery() string { func getClearUserFolderMappingQuery() string {
@ -557,7 +569,7 @@ func getAddUserFolderMappingQuery() string {
func getFoldersQuery(order string, minimal bool) string { func getFoldersQuery(order string, minimal bool) string {
var fieldSelection string var fieldSelection string
if minimal { if minimal {
fieldSelection = "id,name" fieldSelection = selectMinimalFields
} else { } else {
fieldSelection = selectFolderFields fieldSelection = selectFolderFields
} }
@ -593,7 +605,7 @@ func getRelatedGroupsForUsersQuery(users []User) string {
sb.WriteString(")") sb.WriteString(")")
} }
return fmt.Sprintf(`SELECT g.name,ug.group_type,ug.user_id FROM %s g INNER JOIN %s ug ON g.id = ug.group_id WHERE return fmt.Sprintf(`SELECT g.name,ug.group_type,ug.user_id FROM %s g INNER JOIN %s ug ON g.id = ug.group_id WHERE
ug.user_id IN %s ORDER BY ug.user_id`, getSQLTableGroups(), sqlTableUsersGroupsMapping, sb.String()) ug.user_id IN %s ORDER BY ug.user_id`, getSQLQuotedName(sqlTableGroups), sqlTableUsersGroupsMapping, sb.String())
} }
func getRelatedFoldersForUsersQuery(users []User) string { func getRelatedFoldersForUsersQuery(users []User) string {
@ -645,7 +657,8 @@ func getRelatedGroupsForFoldersQuery(folders []vfs.BaseVirtualFolder) string {
sb.WriteString(")") sb.WriteString(")")
} }
return fmt.Sprintf(`SELECT fm.folder_id,g.name FROM %s fm INNER JOIN %s g ON fm.group_id = g.id return fmt.Sprintf(`SELECT fm.folder_id,g.name FROM %s fm INNER JOIN %s g ON fm.group_id = g.id
WHERE fm.folder_id IN %s ORDER BY fm.folder_id`, sqlTableGroupsFoldersMapping, getSQLTableGroups(), sb.String()) WHERE fm.folder_id IN %s ORDER BY fm.folder_id`, sqlTableGroupsFoldersMapping, getSQLQuotedName(sqlTableGroups),
sb.String())
} }
func getRelatedUsersForGroupsQuery(groups []Group) string { func getRelatedUsersForGroupsQuery(groups []Group) string {
@ -711,6 +724,156 @@ func getCleanupActiveTransfersQuery() string {
return fmt.Sprintf(`DELETE FROM %s WHERE updated_at < %s`, sqlTableActiveTransfers, sqlPlaceholders[0]) return fmt.Sprintf(`DELETE FROM %s WHERE updated_at < %s`, sqlTableActiveTransfers, sqlPlaceholders[0])
} }
func getRelatedRulesForActionsQuery(actions []BaseEventAction) string {
var sb strings.Builder
for _, a := range actions {
if sb.Len() == 0 {
sb.WriteString("(")
} else {
sb.WriteString(",")
}
sb.WriteString(strconv.FormatInt(a.ID, 10))
}
if sb.Len() > 0 {
sb.WriteString(")")
}
return fmt.Sprintf(`SELECT am.action_id,r.name FROM %s am INNER JOIN %s r ON am.rule_id = r.id
WHERE am.action_id IN %s ORDER BY r.name ASC`, sqlTableRulesActionsMapping, sqlTableEventsRules, sb.String())
}
func getEventsActionsQuery(order string, minimal bool) string {
var fieldSelection string
if minimal {
fieldSelection = selectMinimalFields
} else {
fieldSelection = selectEventActionFields
}
return fmt.Sprintf(`SELECT %s FROM %s ORDER BY name %s LIMIT %s OFFSET %s`, fieldSelection,
sqlTableEventsActions, order, sqlPlaceholders[0], sqlPlaceholders[1])
}
func getDumpEventActionsQuery() string {
return fmt.Sprintf(`SELECT %s FROM %s`, selectEventActionFields, sqlTableEventsActions)
}
func getEventActionByNameQuery() string {
return fmt.Sprintf(`SELECT %s FROM %s WHERE name = %s`, selectEventActionFields, sqlTableEventsActions,
sqlPlaceholders[0])
}
func getAddEventActionQuery() string {
return fmt.Sprintf(`INSERT INTO %s (name,description,type,options) VALUES (%s,%s,%s,%s)`,
sqlTableEventsActions, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])
}
func getUpdateEventActionQuery() string {
return fmt.Sprintf(`UPDATE %s SET description=%s,type=%s,options=%s WHERE name = %s`, sqlTableEventsActions,
sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])
}
func getDeleteEventActionQuery() string {
return fmt.Sprintf(`DELETE FROM %s WHERE name = %s`, sqlTableEventsActions, sqlPlaceholders[0])
}
func getEventRulesQuery(order string) string {
return fmt.Sprintf(`SELECT %s FROM %s WHERE deleted_at = 0 ORDER BY name %s LIMIT %s OFFSET %s`,
getSelectEventRuleFields(), sqlTableEventsRules, order, sqlPlaceholders[0], sqlPlaceholders[1])
}
func getDumpEventRulesQuery() string {
return fmt.Sprintf(`SELECT %s FROM %s WHERE deleted_at = 0`, getSelectEventRuleFields(), sqlTableEventsRules)
}
func getRecentlyUpdatedRulesQuery() string {
return fmt.Sprintf(`SELECT %s FROM %s WHERE updated_at >= %s OR deleted_at > 0`, getSelectEventRuleFields(),
sqlTableEventsRules, sqlPlaceholders[0])
}
func getEventRulesByNameQuery() string {
return fmt.Sprintf(`SELECT %s FROM %s WHERE name = %s AND deleted_at = 0`, getSelectEventRuleFields(), sqlTableEventsRules,
sqlPlaceholders[0])
}
func getAddEventRuleQuery() string {
return fmt.Sprintf(`INSERT INTO %s (name,description,created_at,updated_at,%s,conditions,deleted_at)
VALUES (%s,%s,%s,%s,%s,%s,0)`,
sqlTableEventsRules, getSQLQuotedName("trigger"), sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2],
sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5])
}
func getUpdateEventRuleQuery() string {
return fmt.Sprintf(`UPDATE %s SET description=%s,updated_at=%s,%s=%s,conditions=%s WHERE name = %s`,
sqlTableEventsRules, sqlPlaceholders[0], sqlPlaceholders[1], getSQLQuotedName("trigger"), sqlPlaceholders[2],
sqlPlaceholders[3], sqlPlaceholders[4])
}
func getDeleteEventRuleQuery(softDelete bool) string {
if softDelete {
return fmt.Sprintf(`UPDATE %s SET updated_at=%s,deleted_at=%s WHERE name = %s`,
sqlTableEventsRules, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2])
}
return fmt.Sprintf(`DELETE FROM %s WHERE name = %s`, sqlTableEventsRules, sqlPlaceholders[0])
}
func getClearRuleActionMappingQuery() string {
return fmt.Sprintf(`DELETE FROM %s WHERE rule_id = (SELECT id FROM %s WHERE name = %s)`, sqlTableRulesActionsMapping,
sqlTableEventsRules, sqlPlaceholders[0])
}
func getUpdateRulesTimestampQuery() string {
return fmt.Sprintf(`UPDATE %s SET updated_at=%s WHERE id IN (SELECT rule_id FROM %s WHERE action_id = %s)`,
sqlTableEventsRules, sqlPlaceholders[0], sqlTableRulesActionsMapping, sqlPlaceholders[1])
}
func getRelatedActionsForRulesQuery(rules []EventRule) string {
var sb strings.Builder
for _, r := range rules {
if sb.Len() == 0 {
sb.WriteString("(")
} else {
sb.WriteString(",")
}
sb.WriteString(strconv.FormatInt(r.ID, 10))
}
if sb.Len() > 0 {
sb.WriteString(")")
}
return fmt.Sprintf(`SELECT a.id,a.name,a.description,a.type,a.options,am.options,am.%s,
am.rule_id FROM %s a INNER JOIN %s am ON a.id = am.action_id WHERE am.rule_id IN %s ORDER BY am.%s ASC`,
getSQLQuotedName("order"), sqlTableEventsActions, sqlTableRulesActionsMapping, sb.String(),
getSQLQuotedName("order"))
}
func getAddRuleActionMappingQuery() string {
return fmt.Sprintf(`INSERT INTO %s (rule_id,action_id,%s,options) VALUES ((SELECT id FROM %s WHERE name = %s),
(SELECT id FROM %s WHERE name = %s),%s,%s)`,
sqlTableRulesActionsMapping, getSQLQuotedName("order"), sqlTableEventsRules, sqlPlaceholders[0],
sqlTableEventsActions, sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])
}
func getTaskByNameQuery() string {
return fmt.Sprintf(`SELECT updated_at,version FROM %s WHERE name = %s`, sqlTableTasks, sqlPlaceholders[0])
}
func getAddTaskQuery() string {
return fmt.Sprintf(`INSERT INTO %s (name,updated_at,version) VALUES (%s,%s,0)`,
sqlTableTasks, sqlPlaceholders[0], sqlPlaceholders[1])
}
func getUpdateTaskQuery() string {
return fmt.Sprintf(`UPDATE %s SET updated_at=%s,version = version + 1 WHERE name = %s AND version = %s`,
sqlTableTasks, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2])
}
func getUpdateTaskTimestampQuery() string {
return fmt.Sprintf(`UPDATE %s SET updated_at=%s WHERE name = %s`,
sqlTableTasks, sqlPlaceholders[0], sqlPlaceholders[1])
}
func getDeleteTaskQuery() string {
return fmt.Sprintf(`DELETE FROM %s WHERE name = %s`, sqlTableTasks, sqlPlaceholders[0])
}
func getDatabaseVersionQuery() string { func getDatabaseVersionQuery() string {
return fmt.Sprintf("SELECT version from %s LIMIT 1", sqlTableSchemaVersion) return fmt.Sprintf("SELECT version from %s LIMIT 1", sqlTableSchemaVersion)
} }

View file

@ -1524,10 +1524,11 @@ func (u *User) applyGroupSettings(groupsMapping map[string]Group) {
if u.groupSettingsApplied { if u.groupSettingsApplied {
return return
} }
replacer := u.getGroupPlacehodersReplacer()
for _, g := range u.Groups { for _, g := range u.Groups {
if g.Type == sdk.GroupTypePrimary { if g.Type == sdk.GroupTypePrimary {
if group, ok := groupsMapping[g.Name]; ok { if group, ok := groupsMapping[g.Name]; ok {
u.mergeWithPrimaryGroup(group) u.mergeWithPrimaryGroup(group, replacer)
} else { } else {
providerLog(logger.LevelError, "mapping not found for user %s, group %s", u.Username, g.Name) providerLog(logger.LevelError, "mapping not found for user %s, group %s", u.Username, g.Name)
} }
@ -1537,7 +1538,7 @@ func (u *User) applyGroupSettings(groupsMapping map[string]Group) {
for _, g := range u.Groups { for _, g := range u.Groups {
if g.Type == sdk.GroupTypeSecondary { if g.Type == sdk.GroupTypeSecondary {
if group, ok := groupsMapping[g.Name]; ok { if group, ok := groupsMapping[g.Name]; ok {
u.mergeAdditiveProperties(group, sdk.GroupTypeSecondary) u.mergeAdditiveProperties(group, sdk.GroupTypeSecondary, replacer)
} else { } else {
providerLog(logger.LevelError, "mapping not found for user %s, group %s", u.Username, g.Name) providerLog(logger.LevelError, "mapping not found for user %s, group %s", u.Username, g.Name)
} }
@ -1566,10 +1567,11 @@ func (u *User) LoadAndApplyGroupSettings() error {
if err != nil { if err != nil {
return fmt.Errorf("unable to get groups: %w", err) return fmt.Errorf("unable to get groups: %w", err)
} }
replacer := u.getGroupPlacehodersReplacer()
// make sure to always merge with the primary group first // make sure to always merge with the primary group first
for idx, g := range groups { for idx, g := range groups {
if g.Name == primaryGroupName { if g.Name == primaryGroupName {
u.mergeWithPrimaryGroup(g) u.mergeWithPrimaryGroup(g, replacer)
lastIdx := len(groups) - 1 lastIdx := len(groups) - 1
groups[idx] = groups[lastIdx] groups[idx] = groups[lastIdx]
groups = groups[:lastIdx] groups = groups[:lastIdx]
@ -1577,42 +1579,46 @@ func (u *User) LoadAndApplyGroupSettings() error {
} }
} }
for _, g := range groups { for _, g := range groups {
u.mergeAdditiveProperties(g, sdk.GroupTypeSecondary) u.mergeAdditiveProperties(g, sdk.GroupTypeSecondary, replacer)
} }
u.removeDuplicatesAfterGroupMerge() u.removeDuplicatesAfterGroupMerge()
return nil return nil
} }
func (u *User) replacePlaceholder(value string) string { func (u *User) getGroupPlacehodersReplacer() *strings.Replacer {
return strings.NewReplacer("%username%", u.Username)
}
func (u *User) replacePlaceholder(value string, replacer *strings.Replacer) string {
if value == "" { if value == "" {
return value return value
} }
return strings.ReplaceAll(value, "%username%", u.Username) return replacer.Replace(value)
} }
func (u *User) replaceFsConfigPlaceholders(fsConfig vfs.Filesystem) vfs.Filesystem { func (u *User) replaceFsConfigPlaceholders(fsConfig vfs.Filesystem, replacer *strings.Replacer) vfs.Filesystem {
switch fsConfig.Provider { switch fsConfig.Provider {
case sdk.S3FilesystemProvider: case sdk.S3FilesystemProvider:
fsConfig.S3Config.KeyPrefix = u.replacePlaceholder(fsConfig.S3Config.KeyPrefix) fsConfig.S3Config.KeyPrefix = u.replacePlaceholder(fsConfig.S3Config.KeyPrefix, replacer)
case sdk.GCSFilesystemProvider: case sdk.GCSFilesystemProvider:
fsConfig.GCSConfig.KeyPrefix = u.replacePlaceholder(fsConfig.GCSConfig.KeyPrefix) fsConfig.GCSConfig.KeyPrefix = u.replacePlaceholder(fsConfig.GCSConfig.KeyPrefix, replacer)
case sdk.AzureBlobFilesystemProvider: case sdk.AzureBlobFilesystemProvider:
fsConfig.AzBlobConfig.KeyPrefix = u.replacePlaceholder(fsConfig.AzBlobConfig.KeyPrefix) fsConfig.AzBlobConfig.KeyPrefix = u.replacePlaceholder(fsConfig.AzBlobConfig.KeyPrefix, replacer)
case sdk.SFTPFilesystemProvider: case sdk.SFTPFilesystemProvider:
fsConfig.SFTPConfig.Username = u.replacePlaceholder(fsConfig.SFTPConfig.Username) fsConfig.SFTPConfig.Username = u.replacePlaceholder(fsConfig.SFTPConfig.Username, replacer)
fsConfig.SFTPConfig.Prefix = u.replacePlaceholder(fsConfig.SFTPConfig.Prefix) fsConfig.SFTPConfig.Prefix = u.replacePlaceholder(fsConfig.SFTPConfig.Prefix, replacer)
case sdk.HTTPFilesystemProvider: case sdk.HTTPFilesystemProvider:
fsConfig.HTTPConfig.Username = u.replacePlaceholder(fsConfig.HTTPConfig.Username) fsConfig.HTTPConfig.Username = u.replacePlaceholder(fsConfig.HTTPConfig.Username, replacer)
} }
return fsConfig return fsConfig
} }
func (u *User) mergeWithPrimaryGroup(group Group) { func (u *User) mergeWithPrimaryGroup(group Group, replacer *strings.Replacer) {
if group.UserSettings.HomeDir != "" { if group.UserSettings.HomeDir != "" {
u.HomeDir = u.replacePlaceholder(group.UserSettings.HomeDir) u.HomeDir = u.replacePlaceholder(group.UserSettings.HomeDir, replacer)
} }
if group.UserSettings.FsConfig.Provider != 0 { if group.UserSettings.FsConfig.Provider != 0 {
u.FsConfig = u.replaceFsConfigPlaceholders(group.UserSettings.FsConfig) u.FsConfig = u.replaceFsConfigPlaceholders(group.UserSettings.FsConfig, replacer)
} }
if u.MaxSessions == 0 { if u.MaxSessions == 0 {
u.MaxSessions = group.UserSettings.MaxSessions u.MaxSessions = group.UserSettings.MaxSessions
@ -1634,11 +1640,11 @@ func (u *User) mergeWithPrimaryGroup(group Group) {
u.DownloadDataTransfer = group.UserSettings.DownloadDataTransfer u.DownloadDataTransfer = group.UserSettings.DownloadDataTransfer
u.TotalDataTransfer = group.UserSettings.TotalDataTransfer u.TotalDataTransfer = group.UserSettings.TotalDataTransfer
} }
u.mergePrimaryGroupFilters(group.UserSettings.Filters) u.mergePrimaryGroupFilters(group.UserSettings.Filters, replacer)
u.mergeAdditiveProperties(group, sdk.GroupTypePrimary) u.mergeAdditiveProperties(group, sdk.GroupTypePrimary, replacer)
} }
func (u *User) mergePrimaryGroupFilters(filters sdk.BaseUserFilters) { func (u *User) mergePrimaryGroupFilters(filters sdk.BaseUserFilters, replacer *strings.Replacer) {
if u.Filters.MaxUploadFileSize == 0 { if u.Filters.MaxUploadFileSize == 0 {
u.Filters.MaxUploadFileSize = filters.MaxUploadFileSize u.Filters.MaxUploadFileSize = filters.MaxUploadFileSize
} }
@ -1664,14 +1670,14 @@ func (u *User) mergePrimaryGroupFilters(filters sdk.BaseUserFilters) {
u.Filters.ExternalAuthCacheTime = filters.ExternalAuthCacheTime u.Filters.ExternalAuthCacheTime = filters.ExternalAuthCacheTime
} }
if u.Filters.StartDirectory == "" { if u.Filters.StartDirectory == "" {
u.Filters.StartDirectory = u.replacePlaceholder(filters.StartDirectory) u.Filters.StartDirectory = u.replacePlaceholder(filters.StartDirectory, replacer)
} }
} }
func (u *User) mergeAdditiveProperties(group Group, groupType int) { func (u *User) mergeAdditiveProperties(group Group, groupType int, replacer *strings.Replacer) {
u.mergeVirtualFolders(group, groupType) u.mergeVirtualFolders(group, groupType, replacer)
u.mergePermissions(group, groupType) u.mergePermissions(group, groupType, replacer)
u.mergeFilePatterns(group, groupType) u.mergeFilePatterns(group, groupType, replacer)
u.Filters.BandwidthLimits = append(u.Filters.BandwidthLimits, group.UserSettings.Filters.BandwidthLimits...) u.Filters.BandwidthLimits = append(u.Filters.BandwidthLimits, group.UserSettings.Filters.BandwidthLimits...)
u.Filters.DataTransferLimits = append(u.Filters.DataTransferLimits, group.UserSettings.Filters.DataTransferLimits...) u.Filters.DataTransferLimits = append(u.Filters.DataTransferLimits, group.UserSettings.Filters.DataTransferLimits...)
u.Filters.AllowedIP = append(u.Filters.AllowedIP, group.UserSettings.Filters.AllowedIP...) u.Filters.AllowedIP = append(u.Filters.AllowedIP, group.UserSettings.Filters.AllowedIP...)
@ -1682,7 +1688,7 @@ func (u *User) mergeAdditiveProperties(group Group, groupType int) {
u.Filters.TwoFactorAuthProtocols = append(u.Filters.TwoFactorAuthProtocols, group.UserSettings.Filters.TwoFactorAuthProtocols...) u.Filters.TwoFactorAuthProtocols = append(u.Filters.TwoFactorAuthProtocols, group.UserSettings.Filters.TwoFactorAuthProtocols...)
} }
func (u *User) mergeVirtualFolders(group Group, groupType int) { func (u *User) mergeVirtualFolders(group Group, groupType int, replacer *strings.Replacer) {
if len(group.VirtualFolders) > 0 { if len(group.VirtualFolders) > 0 {
folderPaths := make(map[string]bool) folderPaths := make(map[string]bool)
for _, folder := range u.VirtualFolders { for _, folder := range u.VirtualFolders {
@ -1692,17 +1698,17 @@ func (u *User) mergeVirtualFolders(group Group, groupType int) {
if folder.VirtualPath == "/" && groupType != sdk.GroupTypePrimary { if folder.VirtualPath == "/" && groupType != sdk.GroupTypePrimary {
continue continue
} }
folder.VirtualPath = u.replacePlaceholder(folder.VirtualPath) folder.VirtualPath = u.replacePlaceholder(folder.VirtualPath, replacer)
if _, ok := folderPaths[folder.VirtualPath]; !ok { if _, ok := folderPaths[folder.VirtualPath]; !ok {
folder.MappedPath = u.replacePlaceholder(folder.MappedPath) folder.MappedPath = u.replacePlaceholder(folder.MappedPath, replacer)
folder.FsConfig = u.replaceFsConfigPlaceholders(folder.FsConfig) folder.FsConfig = u.replaceFsConfigPlaceholders(folder.FsConfig, replacer)
u.VirtualFolders = append(u.VirtualFolders, folder) u.VirtualFolders = append(u.VirtualFolders, folder)
} }
} }
} }
} }
func (u *User) mergePermissions(group Group, groupType int) { func (u *User) mergePermissions(group Group, groupType int, replacer *strings.Replacer) {
for k, v := range group.UserSettings.Permissions { for k, v := range group.UserSettings.Permissions {
if k == "/" { if k == "/" {
if groupType == sdk.GroupTypePrimary { if groupType == sdk.GroupTypePrimary {
@ -1711,14 +1717,14 @@ func (u *User) mergePermissions(group Group, groupType int) {
continue continue
} }
} }
k = u.replacePlaceholder(k) k = u.replacePlaceholder(k, replacer)
if _, ok := u.Permissions[k]; !ok { if _, ok := u.Permissions[k]; !ok {
u.Permissions[k] = v u.Permissions[k] = v
} }
} }
} }
func (u *User) mergeFilePatterns(group Group, groupType int) { func (u *User) mergeFilePatterns(group Group, groupType int, replacer *strings.Replacer) {
if len(group.UserSettings.Filters.FilePatterns) > 0 { if len(group.UserSettings.Filters.FilePatterns) > 0 {
patternPaths := make(map[string]bool) patternPaths := make(map[string]bool)
for _, pattern := range u.Filters.FilePatterns { for _, pattern := range u.Filters.FilePatterns {
@ -1728,7 +1734,7 @@ func (u *User) mergeFilePatterns(group Group, groupType int) {
if pattern.Path == "/" && groupType != sdk.GroupTypePrimary { if pattern.Path == "/" && groupType != sdk.GroupTypePrimary {
continue continue
} }
pattern.Path = u.replacePlaceholder(pattern.Path) pattern.Path = u.replacePlaceholder(pattern.Path, replacer)
if _, ok := patternPaths[pattern.Path]; !ok { if _, ok := patternPaths[pattern.Path]; !ok {
u.Filters.FilePatterns = append(u.Filters.FilePatterns, pattern) u.Filters.FilePatterns = append(u.Filters.FilePatterns, pattern)
} }

View file

@ -62,7 +62,7 @@ If the `hook` defines an HTTP URL then this URL will be invoked as HTTP POST. Th
- `virtual_target_path`, string, virtual target path, seen by SFTPGo users - `virtual_target_path`, string, virtual target path, seen by SFTPGo users
- `ssh_cmd`, string, included for `ssh_cmd` action - `ssh_cmd`, string, included for `ssh_cmd` action
- `file_size`, int64, included for `pre-upload`, `upload`, `download`, `delete` actions if the file size is greater than `0` - `file_size`, int64, included for `pre-upload`, `upload`, `download`, `delete` actions if the file size is greater than `0`
- `fs_provider`, integer, `0` for local filesystem, `1` for S3 backend, `2` for Google Cloud Storage (GCS) backend, `3` for Azure Blob Storage backend, `4` for local encrypted backend, `5` for SFTP backend - `fs_provider`, integer, `0` for local filesystem, `1` for S3 backend, `2` for Google Cloud Storage (GCS) backend, `3` for Azure Blob Storage backend, `4` for local encrypted backend, `5` for SFTP backend, `6` for HTTPFs backend
- `bucket`, string, included for S3, GCS and Azure backends - `bucket`, string, included for S3, GCS and Azure backends
- `endpoint`, string, included for S3, SFTP and Azure backend if configured - `endpoint`, string, included for S3, SFTP and Azure backend if configured
- `status`, integer. Status for `upload`, `download` and `ssh_cmd` actions. 1 means no error, 2 means a generic error occurred, 3 means quota exceeded error - `status`, integer. Status for `upload`, `download` and `ssh_cmd` actions. 1 means no error, 2 means a generic error occurred, 3 means quota exceeded error
@ -85,8 +85,12 @@ The `actions` struct inside the `data_provider` configuration section allows you
The supported object types are: The supported object types are:
- `user` - `user`
- `group`
- `admin` - `admin`
- `api_key` - `api_key`
- `share`
- `event_action`
- `event_rule`
Actions will not be fired for internal updates, such as the last login or the user quota fields, or after external authentication. Actions will not be fired for internal updates, such as the last login or the user quota fields, or after external authentication.

47
docs/eventmanager.md Normal file
View file

@ -0,0 +1,47 @@
# Event Manager
The Event Manager allows an administrator to configure HTTP notifications, commands execution, email notifications and carry out certain server operations based on server events or schedules.
The following actions are supported:
- `HTTP notification`. You can notify an HTTP/S endpoing via GET, POST, PUT methods. You can define custom headers, query parameters and a body for POST and PUT request. Placeholders are supported for username, body, header and query parameter values.
- `Command execution`. You can launch custom commands passing parameters via environment variables. Placeholders are supported for environment variable values.
- `Email notification`. Placeholders are supported in subject and body. The email will be sent as plain text. For this action to work you have to configure an SMTP server in the SFTPGo configuration file.
- `Backup`. A backup will be saved in the configured backup directory. The backup will contain the week day and the hour in the file name.
- `User quota reset`. The quota used by users will be updated based on current usage.
- `Folder quota reset`. The quota used by virtual folders will be updated based on current usage.
- `Transfer quota reset`. The transfer quota values will be reset to `0`.
The following placeholders are supported:
- `{{Name}}`. Username, folder name or admin username for provider actions.
- `{{Event}}`. Event name, for example `upload`, `download` for filesystem events or `add`, `update` for provider events.
- `{{Status}}`. Status for `upload`, `download` and `ssh_cmd` events. 1 means no error, 2 means a generic error occurred, 3 means quota exceeded error.
- `{{VirtualPath}}`. Path seen by SFTPGo users, for example `/adir/afile.txt`.
- `{{FsPath}}`. Full filesystem path, for example `/user/homedir/adir/afile.txt` or `C:/data/user/homedir/adir/afile.txt` on Windows.
- `{{ObjectName}}`. File/directory name, for example `afile.txt` or provider object name.
- `{{ObjectType}}`. Object type for provider events: `user`, `group`, `admin`, etc.
- `{{VirtualTargetPath}}`. Virtual target path for renames.
- `{{FsTargetPath}}`. Full filesystem target path for renames.
- `{{FileSize}}`. File size.
- `{{Protocol}}`. Used protocol, for example `SFTP`, `FTP`.
- `{{IP}}`. Client IP address.
- `{{Timestamp}}`. Event timestamp as nanoseconds since epoch.
- `{{ObjectData}}`. Provider object data serialized as JSON with sensitive fields removed.
Event rules are based on the premise that an event occours. To each rule you can associate one or more actions.
The following trigger events are supported:
- `Filesystem events`, for example `upload`, `download` etc.
- `Provider events`, for example add/update/delete user.
- `Schedules`.
You can further restrict a rule by specifying additional conditions that must be met before the rules actions are taken. For example you can react to uploads only if they are performed by a particular user or using a specified protocol.
Actions are executed in a sequential order. For each action associated to a rule you can define the following settings:
- `Stop on failure`, the next action will not be executed if the current one fails.
- `Failure action`, this action will be executed only if at least another one fails.
- `Execute sync`, for upload events, you can execute the action synchronously. Executing an action synchronously means that SFTPGo will not return a result code to the client (which is waiting for it) until your action have completed its execution. If your acion takes a long time to complete this could cause a timeout on the client side, which wouldn't receive the server response in a timely manner and eventually drop the connection.
If you are running multiple SFTPGo instances connected to the same data provider, you can choose whether to allow simultaneous execution for scheduled actions.

View file

@ -247,12 +247,8 @@ The configuration file contains the following sections:
- `update_mode`, integer. Defines how the database will be initialized/updated. 0 means automatically. 1 means manually using the initprovider sub-command. - `update_mode`, integer. Defines how the database will be initialized/updated. 0 means automatically. 1 means manually using the initprovider sub-command.
- `create_default_admin`, boolean. Before you can use SFTPGo you need to create an admin account. If you open the admin web UI, a setup screen will guide you in creating the first admin account. You can automatically create the first admin account by enabling this setting and setting the environment variables `SFTPGO_DEFAULT_ADMIN_USERNAME` and `SFTPGO_DEFAULT_ADMIN_PASSWORD`. You can also create the first admin by loading initial data. This setting has no effect if an admin account is already found within the data provider. Default `false`. - `create_default_admin`, boolean. Before you can use SFTPGo you need to create an admin account. If you open the admin web UI, a setup screen will guide you in creating the first admin account. You can automatically create the first admin account by enabling this setting and setting the environment variables `SFTPGO_DEFAULT_ADMIN_USERNAME` and `SFTPGO_DEFAULT_ADMIN_PASSWORD`. You can also create the first admin by loading initial data. This setting has no effect if an admin account is already found within the data provider. Default `false`.
- `naming_rules`, integer. Naming rules for usernames, folder and group names. `0` means no rules. `1` means you can use any UTF-8 character. The names are used in URIs for REST API and Web admin. If not set only unreserved URI characters are allowed: ALPHA / DIGIT / "-" / "." / "_" / "~". `2` means names are converted to lowercase before saving/matching and so case insensitive matching is possible. `3` means trimming trailing and leading white spaces before saving/matching. Rules can be combined, for example `3` means both converting to lowercase and allowing any UTF-8 character. Enabling these options for existing installations could be backward incompatible, some users could be unable to login, for example existing users with mixed cases in their usernames. You have to ensure that all existing users respect the defined rules. Default: `1`. - `naming_rules`, integer. Naming rules for usernames, folder and group names. `0` means no rules. `1` means you can use any UTF-8 character. The names are used in URIs for REST API and Web admin. If not set only unreserved URI characters are allowed: ALPHA / DIGIT / "-" / "." / "_" / "~". `2` means names are converted to lowercase before saving/matching and so case insensitive matching is possible. `3` means trimming trailing and leading white spaces before saving/matching. Rules can be combined, for example `3` means both converting to lowercase and allowing any UTF-8 character. Enabling these options for existing installations could be backward incompatible, some users could be unable to login, for example existing users with mixed cases in their usernames. You have to ensure that all existing users respect the defined rules. Default: `1`.
- `is_shared`, integer. If the data provider is shared across multiple SFTPGo instances, set this parameter to `1`. `MySQL`, `PostgreSQL` and `CockroachDB` can be shared, this setting is ignored for other data providers. For shared data providers, active transfers are persisted in the database and thus quota checks between ongoing transfers will work cross multiple instances. Password reset requests and OIDC tokens/states are also persisted in the database if the provider is shared. The database table `shared_sessions` is used only to store temporary sessions. In performance critical installations, you might consider using a database-specific optimization, for example you might use an `UNLOGGED` table for PostgreSQL. This optimization in only required in very limited use cases. Default: `0`. - `is_shared`, integer. If the data provider is shared across multiple SFTPGo instances, set this parameter to `1`. `MySQL`, `PostgreSQL` and `CockroachDB` can be shared, this setting is ignored for other data providers. For shared data providers, active transfers are persisted in the database and thus quota checks between ongoing transfers will work cross multiple instances. Password reset requests and OIDC tokens/states are also persisted in the database if the provider is shared. For shared data providers, scheduled event actions are only executed on a single SFTPGo instance by default, you can override this behavior on a per-action basis. The database table `shared_sessions` is used only to store temporary sessions. In performance critical installations, you might consider using a database-specific optimization, for example you might use an `UNLOGGED` table for PostgreSQL. This optimization in only required in very limited use cases. Default: `0`.
- `backups_path`, string. Path to the backup directory. This can be an absolute path or a path relative to the config dir. We don't allow backups in arbitrary paths for security reasons. - `backups_path`, string. Path to the backup directory. This can be an absolute path or a path relative to the config dir. We don't allow backups in arbitrary paths for security reasons.
- `auto_backup`, struct. Defines the configuration for automatic data provider backups. Example: hour `0` and day_of_week `*` means a backup every day at midnight. The backup file name is in the format `backup_<day_of_week>_<hour>.json`, files with the same name will be overwritten. Note, this process will only backup provider data (users, folders, shares, admins, api keys) and will not backup the configuration file and users files.
- `enabled`, boolean. Set to `true` to enable automatic backups. Default: `true`.
- `hour`, string. Hour as standard cron expression. Allowed values: 0-23. Allowed special characters: asterisk (`*`), slash (`/`), comma (`,`), hyphen (`-`). More info about special characters [here](https://pkg.go.dev/github.com/robfig/cron#hdr-Special_Characters). Default: `0`.
- `day_of_week`, string. Day of week as standard cron expression. Allowed values: 0-6 (Sunday to Saturday). Allowed special characters: asterisk (`*`), slash (`/`), comma (`,`), hyphen (`-`), question mark (`?`). More info about special characters [here](https://pkg.go.dev/github.com/robfig/cron#hdr-Special_Characters). Default: `*`.
- **"httpd"**, the configuration for the HTTP server used to serve REST API and to expose the built-in web interface - **"httpd"**, the configuration for the HTTP server used to serve REST API and to expose the built-in web interface
- `bindings`, list of structs. Each struct has the following fields: - `bindings`, list of structs. Each struct has the following fields:
- `port`, integer. The port used for serving HTTP requests. Default: 8080. - `port`, integer. The port used for serving HTTP requests. Default: 8080.

View file

@ -77,154 +77,154 @@ CzgWkxiz7XE4lgUwX44FCXZM3+JeUbI=
-----END EC PRIVATE KEY-----` -----END EC PRIVATE KEY-----`
caCRT = `-----BEGIN CERTIFICATE----- caCRT = `-----BEGIN CERTIFICATE-----
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
r3rwjFsQOoZotA== WwSa13hh6yjoDQ==
-----END CERTIFICATE-----` -----END CERTIFICATE-----`
caCRL = `-----BEGIN X509 CRL----- caCRL = `-----BEGIN X509 CRL-----
MIICpzCBkAIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0QXV0aBcN MIICpzCBkAIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0QXV0aBcN
MjEwMTAyMjEzNDA1WhcNMjMwMTAyMjEzNDA1WjAkMCICEQC+l04DbHWMyC3fG09k MjIwNzA0MTU1MzU4WhcNMjQwNzAzMTU1MzU4WjAkMCICEQDZo5Q3lhxFuDUsxGNm
VXf+Fw0yMTAxMDIyMTM0MDVaoCMwITAfBgNVHSMEGDAWgBTtcgjQBq/80EySIvJc 794YFw0yMjA3MDQxNTUzNThaoCMwITAfBgNVHSMEGDAWgBT4qzwTztNc1Ln+8Qvn
N8OTorgbzDANBgkqhkiG9w0BAQsFAAOCAgEAEJ7z+uNc8sqtxlOhSdTGDzX/xput bGzjmOULwDANBgkqhkiG9w0BAQsFAAOCAgEA1lK6g8qmhyY6myx8342dDuaauY03
E857kFQkSlMnU2whQ8c+XpYrBLA5vIZJNSSwohTpM4+zVBX/bJpmu3wqqaArRO9/ 0iojkxpasuYcytK6XRm96YqjZK9EETxsHHViVU0vCXES60D6wJ9gw4fTWn3WxEdx
YcW5mQk9Anvb4WjQW1cHmtNapMTzoC9AiYt/OWPfy+P6JCgCr4Hy6LgQyIRL6bM9 nIwbGyjUGHh2y+R3uQsfvwxsdYvDsTLAnOLwOo68dAHWmMDZRmgTuGNoYFxVQRGR
VYTalolOm1qa4Y5cIeT7iHq/91mfaqo8/6MYRjLl8DOTROpmw8OS9bCXkzGKdCat Cn90ZR7LPLpCScclWM8FE/W1B90x3ZE8EhJiCI/WyyTh3EgshmB7A5GoDrFZfmvR
AbAzwkQUSauyoCQ10rpX+Y64w9ng3g4Dr20aCqPf5osaqplEJ2HTK8ljDTidlslv dzoTKO+F9p2XjtmgfiBE3czWQysfATmbutZUbG/ZRb89u+ZEUyPoC94mg8fhNWoX
9anQj8ax3Su89vI8+hK+YbfVQwrThabgdSjQsn+veyx8GlP8WwHLAQ379KjZjWg+ 1d5G9QAkZFHp957/5QHLq9OHNfnWXoohhebjF4VWqZH7w+RtLc8t0PIog2lX4t1o
OlOSwBeU1vTdP0QcB8X5C2gVujAyuQekbaV86xzIBOj7vZdfHZ6ee30TZ2FKiMyg 5N/xFk9akvuoyNGg/fYuJBmN162Q0MdeYfYKDGWdXxf6fpHxVr5v2JrIx6gOwubb
7/N2OqW0w77ChsjB4MSHJCfuTgIeg62GzuZXLM+Q2Z9LBdtm4Byg+sm/P52adOEg cIKP22ZBv/PYOeFsAZ755lTl4OTFUjU5ZJEPD6pUc1daaIqfxsxu8gDZP92FZjsB
gVb2Zf4KSvsAmA0PIBlu449/QXUFcMxzLFy7mwTeZj2B4Ln0Hm0szV9f9R8MwMtB zaalMbh30n2OhagSMBzSLg5rE6WmBzlQX0ZN8YrW4l2Vq6twnnFHY+UyblRZS+d4
SyLYxVH+mgqaR6Jkk22Q/yYyLPaELfafX5gp/AIXG8n0zxfVaTvK3auSgb1Q6ZLS oHBaoOaxPEkLxNZ8ulzJS4B6c4D1CXOaBEf++snVzRRUOEdX3x7TvkkrLvIsm06R
5QH9dSIsmZHlPq7GoSXmKpMdjUL8eaky/IMteioyXgsBiATzl5L2dsw6MTX3MDF0 ux0L1zJb9LbZ/1rhuv70z/kIlD55sqYuRqu3RpgTgZuTERU//rYIqWd03Y5Qon8i
QbDK+MzhmbKfDxs= VoC6Yp9DPldQJrk=
-----END X509 CRL-----` -----END X509 CRL-----`
client1Crt = `-----BEGIN CERTIFICATE----- client1Crt = `-----BEGIN CERTIFICATE-----
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
O/e3EH8= 7LEmxYg=
-----END CERTIFICATE-----` -----END CERTIFICATE-----`
client1Key = `-----BEGIN RSA PRIVATE KEY----- client1Key = `-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----` -----END RSA PRIVATE KEY-----`
// client 2 crt is revoked // client 2 crt is revoked
client2Crt = `-----BEGIN CERTIFICATE----- client2Crt = `-----BEGIN CERTIFICATE-----
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
6zdDidU= enqNrFI=
-----END CERTIFICATE-----` -----END CERTIFICATE-----`
client2Key = `-----BEGIN RSA PRIVATE KEY----- client2Key = `-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----` -----END RSA PRIVATE KEY-----`
testFileName = "test_file_ftp.dat" testFileName = "test_file_ftp.dat"
testDLFileName = "test_download_ftp.dat" testDLFileName = "test_download_ftp.dat"

46
go.mod
View file

@ -4,25 +4,25 @@ go 1.18
require ( require (
cloud.google.com/go/storage v1.23.0 cloud.google.com/go/storage v1.23.0
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.0 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.1
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.4.1 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.4.1
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387 github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
github.com/aws/aws-sdk-go-v2 v1.16.6 github.com/aws/aws-sdk-go-v2 v1.16.7
github.com/aws/aws-sdk-go-v2/config v1.15.12 github.com/aws/aws-sdk-go-v2/config v1.15.13
github.com/aws/aws-sdk-go-v2/credentials v1.12.7 github.com/aws/aws-sdk-go-v2/credentials v1.12.8
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.7 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.8
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.17 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.19
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.7 github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.8
github.com/aws/aws-sdk-go-v2/service/s3 v1.26.12 github.com/aws/aws-sdk-go-v2/service/s3 v1.27.1
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.12 github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.13
github.com/aws/aws-sdk-go-v2/service/sts v1.16.8 github.com/aws/aws-sdk-go-v2/service/sts v1.16.9
github.com/cockroachdb/cockroach-go/v2 v2.2.14 github.com/cockroachdb/cockroach-go/v2 v2.2.14
github.com/coreos/go-oidc/v3 v3.2.0 github.com/coreos/go-oidc/v3 v3.2.0
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001 github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
github.com/fclairamb/ftpserverlib v0.18.1-0.20220515214847-f96d31ec626e github.com/fclairamb/ftpserverlib v0.18.1-0.20220515214847-f96d31ec626e
github.com/fclairamb/go-log v0.3.0 github.com/fclairamb/go-log v0.3.0
github.com/go-acme/lego/v4 v4.7.0 github.com/go-acme/lego/v4 v4.8.0
github.com/go-chi/chi/v5 v5.0.8-0.20220512131524-9e71a0d4b3d6 github.com/go-chi/chi/v5 v5.0.8-0.20220512131524-9e71a0d4b3d6
github.com/go-chi/jwtauth/v5 v5.0.2 github.com/go-chi/jwtauth/v5 v5.0.2
github.com/go-chi/render v1.0.1 github.com/go-chi/render v1.0.1
@ -52,13 +52,13 @@ require (
github.com/rs/xid v1.4.0 github.com/rs/xid v1.4.0
github.com/rs/zerolog v1.27.0 github.com/rs/zerolog v1.27.0
github.com/sftpgo/sdk v0.1.2-0.20220611083241-b653555f7f4d github.com/sftpgo/sdk v0.1.2-0.20220611083241-b653555f7f4d
github.com/shirou/gopsutil/v3 v3.22.5 github.com/shirou/gopsutil/v3 v3.22.6
github.com/spf13/afero v1.8.2 github.com/spf13/afero v1.8.2
github.com/spf13/cobra v1.5.0 github.com/spf13/cobra v1.5.0
github.com/spf13/viper v1.12.0 github.com/spf13/viper v1.12.0
github.com/stretchr/testify v1.8.0 github.com/stretchr/testify v1.8.0
github.com/studio-b12/gowebdav v0.0.0-20220128162035-c7b1ff8a5e62 github.com/studio-b12/gowebdav v0.0.0-20220128162035-c7b1ff8a5e62
github.com/unrolled/secure v1.10.0 github.com/unrolled/secure v1.11.0
github.com/wagslane/go-password-validator v0.3.0 github.com/wagslane/go-password-validator v0.3.0
github.com/xhit/go-simple-mail/v2 v2.11.0 github.com/xhit/go-simple-mail/v2 v2.11.0
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a
@ -68,7 +68,7 @@ require (
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e
golang.org/x/oauth2 v0.0.0-20220630143837-2104d58473e0 golang.org/x/oauth2 v0.0.0-20220630143837-2104d58473e0
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 golang.org/x/time v0.0.0-20220609170525-579cf78fd858
google.golang.org/api v0.86.0 google.golang.org/api v0.86.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0
@ -80,15 +80,15 @@ require (
cloud.google.com/go/iam v0.3.0 // indirect cloud.google.com/go/iam v0.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.3 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.13 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.14 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.7 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.8 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.14 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.3.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.4 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.3 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.8 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.9 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.7 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.8 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.7 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.8 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.11.10 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.11.11 // indirect
github.com/aws/smithy-go v1.12.0 // indirect github.com/aws/smithy-go v1.12.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1 // indirect github.com/boombuler/barcode v1.0.1 // indirect
@ -155,7 +155,7 @@ require (
golang.org/x/tools v0.1.11 // indirect golang.org/x/tools v0.1.11 // indirect
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220630160836-4327a74d660d // indirect google.golang.org/genproto v0.0.0-20220708155623-50e5f4832e73 // indirect
google.golang.org/grpc v1.47.0 // indirect google.golang.org/grpc v1.47.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/ini.v1 v1.66.6 // indirect gopkg.in/ini.v1 v1.66.6 // indirect

93
go.sum
View file

@ -90,8 +90,8 @@ github.com/Azure/azure-sdk-for-go v51.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9mo
github.com/Azure/azure-sdk-for-go v59.3.0+incompatible h1:dPIm0BO4jsMXFcCI/sLTPkBtE7mk8WMuRHA0JeWhlcQ= github.com/Azure/azure-sdk-for-go v59.3.0+incompatible h1:dPIm0BO4jsMXFcCI/sLTPkBtE7mk8WMuRHA0JeWhlcQ=
github.com/Azure/azure-sdk-for-go v59.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v59.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.0 h1:Ut0ZGdOwJDw0npYEg+TLlPls3Pq6JiZaP2/aGKir7Zw= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.1 h1:tz19qLF65vuu2ibfTqGVJxG/zZAI27NEIIbvAOQwYbw=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.1/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.0.0 h1:Yoicul8bnVdQrhDMTHxdEckRGX01XvwXDHUT9zYZ3k0= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.0.0 h1:Yoicul8bnVdQrhDMTHxdEckRGX01XvwXDHUT9zYZ3k0=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
@ -140,64 +140,64 @@ 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.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.43.31/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/aws/aws-sdk-go v1.43.31/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go-v2 v1.16.2/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU= github.com/aws/aws-sdk-go-v2 v1.16.2/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU=
github.com/aws/aws-sdk-go-v2 v1.16.6 h1:kzafGZYwkwVgLZ2zEX7P+vTwLli6uIMXF8aGjunN6UI= github.com/aws/aws-sdk-go-v2 v1.16.7 h1:zfBwXus3u14OszRxGcqCDS4MfMCv10e8SMJ2r8Xm0Ns=
github.com/aws/aws-sdk-go-v2 v1.16.6/go.mod h1:6CpKuLXg2w7If3ABZCl/qZ6rEgwtjZTn4eAf4RcEyuw= github.com/aws/aws-sdk-go-v2 v1.16.7/go.mod h1:6CpKuLXg2w7If3ABZCl/qZ6rEgwtjZTn4eAf4RcEyuw=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1/go.mod h1:n8Bs1ElDD2wJ9kCRTczA83gYbBmjSwZp3umc6zF4EeM= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1/go.mod h1:n8Bs1ElDD2wJ9kCRTczA83gYbBmjSwZp3umc6zF4EeM=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.3 h1:S/ZBwevQkr7gv5YxONYpGQxlMFFYSRfz3RMcjsC9Qhk= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.3 h1:S/ZBwevQkr7gv5YxONYpGQxlMFFYSRfz3RMcjsC9Qhk=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.3/go.mod h1:gNsR5CaXKmQSSzrmGxmwmct/r+ZBfbxorAuXYsj/M5Y= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.3/go.mod h1:gNsR5CaXKmQSSzrmGxmwmct/r+ZBfbxorAuXYsj/M5Y=
github.com/aws/aws-sdk-go-v2/config v1.15.3/go.mod h1:9YL3v07Xc/ohTsxFXzan9ZpFpdTOFl4X65BAKYaz8jg= github.com/aws/aws-sdk-go-v2/config v1.15.3/go.mod h1:9YL3v07Xc/ohTsxFXzan9ZpFpdTOFl4X65BAKYaz8jg=
github.com/aws/aws-sdk-go-v2/config v1.15.12 h1:D4mdf0cOSmZRgJe0DDOd1Qm6tkwHJ7r5i1lz0asa+AA= github.com/aws/aws-sdk-go-v2/config v1.15.13 h1:CJH9zn/Enst7lDiGpoguVt0lZr5HcpNVlRJWbJ6qreo=
github.com/aws/aws-sdk-go-v2/config v1.15.12/go.mod h1:oxRNnH11J580bxDEXyfTqfB3Auo2fxzhV052LD4HnyA= github.com/aws/aws-sdk-go-v2/config v1.15.13/go.mod h1:AcMu50uhV6wMBUlURnEXhr9b3fX6FLSTlEV89krTEGk=
github.com/aws/aws-sdk-go-v2/credentials v1.11.2/go.mod h1:j8YsY9TXTm31k4eFhspiQicfXPLZ0gYXA50i4gxPE8g= github.com/aws/aws-sdk-go-v2/credentials v1.11.2/go.mod h1:j8YsY9TXTm31k4eFhspiQicfXPLZ0gYXA50i4gxPE8g=
github.com/aws/aws-sdk-go-v2/credentials v1.12.7 h1:e2DcCR0gP+T2zVj5eQPMQoRdxo+vd2p9BkpJ72BdyzA= github.com/aws/aws-sdk-go-v2/credentials v1.12.8 h1:niTa7zc7uyOP2ufri0jPESBt1h9yP3Zc0q+xzih3h8o=
github.com/aws/aws-sdk-go-v2/credentials v1.12.7/go.mod h1:8b1nSHdDaKLho9VEK+K8WivifA/2K5pPm4sfI21NlQ8= github.com/aws/aws-sdk-go-v2/credentials v1.12.8/go.mod h1:P2Hd4Sy7mXRxPNcQMPBmqszSJoDXexX8XEDaT6lucO0=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.3/go.mod h1:uk1vhHHERfSVCUnqSqz8O48LBYDSC+k6brng09jcMOk= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.3/go.mod h1:uk1vhHHERfSVCUnqSqz8O48LBYDSC+k6brng09jcMOk=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.7 h1:8yi2ORCwXpXEPnj0vP3DjYhejwDQD/5klgBoxXcKOxY= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.8 h1:VfBdn2AxwMbFyJN/lF/xuT3SakomJ86PZu3rCxb5K0s=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.7/go.mod h1:81k6q0UUZj6AdQZ1E/VQ27cLrTUpJGraZR6/hVHRxjE= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.8/go.mod h1:oL1Q3KuCq1D4NykQnIvtRiBGLUXhcpY5pl6QZB2XEPU=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.3/go.mod h1:0dHuD2HZZSiwfJSy1FO5bX1hQ1TxVV1QXXjpn3XUE44= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.3/go.mod h1:0dHuD2HZZSiwfJSy1FO5bX1hQ1TxVV1QXXjpn3XUE44=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.17 h1:9Y+OvoIvC8KocGNqbbBNDvMu0zsIgzKg3r+ZllSuH5Y= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.19 h1:WfCYqsAADDRNCQQ5LGcrlqbR7SK3PYrP/UCh7qNGBQM=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.17/go.mod h1:z/7g6Z78jPG0l3HeShseUWzA+aBJDK4Mu5DkKkYdIW0= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.19/go.mod h1:koLPv2oF6ksE3zBKLDP0GFmKfaCmYwVHqGIbaPrHIRg=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.13 h1:WuQ1yGs3TMJgxpGVLspcsU/5q1omSA0SG6Cu0yZ4jkM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.14 h1:2C0pYHcUBmdzPj+EKNC4qj97oK6yjrUhc1KoSodglvk=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.13/go.mod h1:wLLesU+LdMZDM3U0PP9vZXJW39zmD/7L4nY2pSrYZ/g= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.14/go.mod h1:kdjrMwHwrC3+FsKhNcCMJ7tUVj/8uSD5CZXeQ4wV6fM=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.3/go.mod h1:ssOhaLpRlh88H3UmEcsBoVKq309quMvm3Ds8e9d4eJM= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.3/go.mod h1:ssOhaLpRlh88H3UmEcsBoVKq309quMvm3Ds8e9d4eJM=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.7 h1:mCeDDYeDXp3loo/xKi7nkx34eeh7q3n1mUBtzptsj8c= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.8 h1:2J+jdlBJWEmTyAwC82Ym68xCykIvnSnIN18b8xHGlcc=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.7/go.mod h1:93Uot80ddyVzSl//xEJreNKMhxntr71WtR3v/A1cRYk= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.8/go.mod h1:ZIV8GYoC6WLBW5KGs+o4rsc65/ozd+eQ0L31XF5VDwk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.10/go.mod h1:8DcYQcz0+ZJaSxANlHIsbbi6S+zMwjwdDqwW3r9AzaE= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.10/go.mod h1:8DcYQcz0+ZJaSxANlHIsbbi6S+zMwjwdDqwW3r9AzaE=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.14 h1:bJv4Y9QOiW0GZPStgLgpGrpdfRDSR3XM4V4M3YCQRZo= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.15 h1:QquxR7NH3ULBsKC+NoTpilzbKKS+5AELfNREInbhvas=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.14/go.mod h1:R1HF8ZDdcRFfAGF+13En4LSHi2IrrNuPQCaxgWCeGyY= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.15/go.mod h1:Tkrthp/0sNBShQQsamR7j/zY4p19tVTAs+nnqhH6R3c=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.4 h1:wusoY1MJ9JNrPoX3n4kxY4MTIUivCiXvTYQbYh59yxs= github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.5 h1:tEEHn+PGAxRVqMPEhtU8oCSW/1Ge3zP5nUgPrGQNUPs=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.4/go.mod h1:cHTMyJVEXRUZ25f8V+pq6CAwoYARarJRFGf3XH4eIxE= github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.5/go.mod h1:aIwFF3dUk95ocCcA3zfk3nhz0oLkpzHFWuMp8l/4nNs=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.1/go.mod h1:GeUru+8VzrTXV/83XyMJ80KpH8xO89VPoUileyNQ+tc= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.1/go.mod h1:GeUru+8VzrTXV/83XyMJ80KpH8xO89VPoUileyNQ+tc=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.3 h1:4n4KCtv5SUoT5Er5XV41huuzrCqepxlW3SDI9qHQebc= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.3 h1:4n4KCtv5SUoT5Er5XV41huuzrCqepxlW3SDI9qHQebc=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.3/go.mod h1:gkb2qADY+OHaGLKNTYxMaQNacfeyQpZ4csDTQMeFmcw= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.3/go.mod h1:gkb2qADY+OHaGLKNTYxMaQNacfeyQpZ4csDTQMeFmcw=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.3/go.mod h1:Seb8KNmD6kVTjwRjVEgOT5hPin6sq+v4C2ycJQDwuH8= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.3/go.mod h1:Seb8KNmD6kVTjwRjVEgOT5hPin6sq+v4C2ycJQDwuH8=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.8 h1:BzBekDihMMeBexBhdK7xS3AIh2Jg/mECyLWO5RRwwHY= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.9 h1:gVv2vXOMqJeR4ZHHV32K7LElIJIIzyw/RU1b0lSfWTQ=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.8/go.mod h1:a1BSeQI9IVr1j5Dwn73cdAKi4MdizTaV9YovUaHefGI= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.9/go.mod h1:EF5RLnD9l0xvEWwMRcktIS/dI6lF8lU5eV3B13k6sWo=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.3/go.mod h1:wlY6SVjuwvh3TVRpTqdy4I1JpBFLX4UGeKZdWntaocw= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.3/go.mod h1:wlY6SVjuwvh3TVRpTqdy4I1JpBFLX4UGeKZdWntaocw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.7 h1:M7/BzQNsu0XXiJRe3gUn8UA8tExF6kLMAfvo5PT/KJY= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.8 h1:oKnAXxSF2FUvfgw8uzU/v9OTYorJJZ8eBmWhr9TWVVQ=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.7/go.mod h1:HvVdEh/x4jsPBsjNvDy+MH3CDCPy4gTZEzFe2r4uJY8= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.8/go.mod h1:rDVhIMAX9N2r8nWxDUlbubvvaFMnfsm+3jAV7q+rpM4=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.3/go.mod h1:Bm/v2IaN6rZ+Op7zX+bOUMdL4fsrYZiD0dsjLhNKwZc= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.3/go.mod h1:Bm/v2IaN6rZ+Op7zX+bOUMdL4fsrYZiD0dsjLhNKwZc=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.7 h1:imb0NhTQZaTDSAQvgFyiZbKTwl0F+AkZL1ZNoEHtuQc= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.8 h1:TlN1UC39A0LUNoD51ubO5h32haznA+oVe15jO9O4Lj0=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.7/go.mod h1:V952z/yIT247sKya+CB+Ls3sxpB9jeBj5TkLraCGKGU= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.8/go.mod h1:JlVwmWtT/1c5W+6oUsjXjAJ0iJZ+hlghdrDy/8JxGCU=
github.com/aws/aws-sdk-go-v2/service/kms v1.16.3/go.mod h1:QuiHPBqlOFCi4LqdSskYYAWpQlx3PKmohy+rE2F+o5g= github.com/aws/aws-sdk-go-v2/service/kms v1.16.3/go.mod h1:QuiHPBqlOFCi4LqdSskYYAWpQlx3PKmohy+rE2F+o5g=
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.7 h1:/5I4p9aytzW1pQfQaCYAqTUzVE48jgJHVFfQVTU4LB8= github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.8 h1:J1muHmn3xAYD1KQpngUX0lCVMu1hJeLbZIihiAwH7ME=
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.7/go.mod h1:DbtPrtO8rAOefDzgqGCqUV+Ub2UtL3/prfQt9bFFYoU= github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.8/go.mod h1:U1XEH0dP8jhhfKNaRktE4WnAjvLtq10fI+CtPLMv/5s=
github.com/aws/aws-sdk-go-v2/service/s3 v1.26.3/go.mod h1:g1qvDuRsJY+XghsV6zg00Z4KJ7DtFFCx8fJD2a491Ak= github.com/aws/aws-sdk-go-v2/service/s3 v1.26.3/go.mod h1:g1qvDuRsJY+XghsV6zg00Z4KJ7DtFFCx8fJD2a491Ak=
github.com/aws/aws-sdk-go-v2/service/s3 v1.26.12 h1:/JTTdNObz+GygQqnbdBzummuxFIcuB6hbra1mqS+Wic= github.com/aws/aws-sdk-go-v2/service/s3 v1.27.1 h1:OKQIQ0QhEBmGr2LfT952meIZz3ujrPYnxH+dO/5ldnI=
github.com/aws/aws-sdk-go-v2/service/s3 v1.26.12/go.mod h1:eas8WnpTDJtCvEjRXAINFuox9TmEGeevxiUKEKv2tQ8= github.com/aws/aws-sdk-go-v2/service/s3 v1.27.1/go.mod h1:NffjpNsMUFXp6Ok/PahrktAncoekWrywvmIK83Q2raE=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4/go.mod h1:PJc8s+lxyU8rrre0/4a0pn2wgwiDvOEzoOjcJUBr67o= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4/go.mod h1:PJc8s+lxyU8rrre0/4a0pn2wgwiDvOEzoOjcJUBr67o=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.12 h1:Pq2GrbfG74dX/JhY/O+bWBz7DUzdgTvqugofqTWLACQ= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.13 h1:9hFlfWKP1+u3js8IhRGf3M+S4MSoDK2v3bqIndGEpxU=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.12/go.mod h1:MfgrkSNjFbMLz19srWgyGJtvDEfXg/ZUJ6AIrxdj65M= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.13/go.mod h1:ByZbrzJwj5ScH6gvAlGslJK/LgJtPd0tteTBoG+yjVc=
github.com/aws/aws-sdk-go-v2/service/sns v1.17.4/go.mod h1:kElt+uCcXxcqFyc+bQqZPFD9DME/eC6oHBXvFzQ9Bcw= github.com/aws/aws-sdk-go-v2/service/sns v1.17.4/go.mod h1:kElt+uCcXxcqFyc+bQqZPFD9DME/eC6oHBXvFzQ9Bcw=
github.com/aws/aws-sdk-go-v2/service/sqs v1.18.3/go.mod h1:skmQo0UPvsjsuYYSYMVmrPc1HWCbHUJyrCEp+ZaLzqM= github.com/aws/aws-sdk-go-v2/service/sqs v1.18.3/go.mod h1:skmQo0UPvsjsuYYSYMVmrPc1HWCbHUJyrCEp+ZaLzqM=
github.com/aws/aws-sdk-go-v2/service/ssm v1.24.1/go.mod h1:NR/xoKjdbRJ+qx0pMR4mI+N/H1I1ynHwXnO6FowXJc0= github.com/aws/aws-sdk-go-v2/service/ssm v1.24.1/go.mod h1:NR/xoKjdbRJ+qx0pMR4mI+N/H1I1ynHwXnO6FowXJc0=
github.com/aws/aws-sdk-go-v2/service/sso v1.11.3/go.mod h1:7UQ/e69kU7LDPtY40OyoHYgRmgfGM4mgsLYtcObdveU= github.com/aws/aws-sdk-go-v2/service/sso v1.11.3/go.mod h1:7UQ/e69kU7LDPtY40OyoHYgRmgfGM4mgsLYtcObdveU=
github.com/aws/aws-sdk-go-v2/service/sso v1.11.10 h1:icon5WWg9Yg5nkB0pJF6bfKw6M0xozukeGKSNKtnqzw= github.com/aws/aws-sdk-go-v2/service/sso v1.11.11 h1:XOJWXNFXJyapJqQuCIPfftsOf0XZZioM0kK6OPRt9MY=
github.com/aws/aws-sdk-go-v2/service/sso v1.11.10/go.mod h1:UHxA35uPrCykRySBV5iSPZhZRlYnWSS2c/aaZVsoU94= github.com/aws/aws-sdk-go-v2/service/sso v1.11.11/go.mod h1:MO4qguFjs3wPGcCSpQ7kOFTwRvb+eu+fn+1vKleGHUk=
github.com/aws/aws-sdk-go-v2/service/sts v1.16.3/go.mod h1:bfBj0iVmsUyUg4weDB4NxktD9rDGeKSVWnjTnwbx9b8= github.com/aws/aws-sdk-go-v2/service/sts v1.16.3/go.mod h1:bfBj0iVmsUyUg4weDB4NxktD9rDGeKSVWnjTnwbx9b8=
github.com/aws/aws-sdk-go-v2/service/sts v1.16.8 h1:GLGfpqX+1bmjNvUJkwB1ZaDpNFXQwJ3z9RkQDA58OBY= github.com/aws/aws-sdk-go-v2/service/sts v1.16.9 h1:yOfILxyjmtr2ubRkRJldlHDFBhf5vw4CzhbwWIBmimQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.16.8/go.mod h1:50YdFq1WIuxA0AGrygvYGucnNYrG24WYzu5fNp7lMgY= github.com/aws/aws-sdk-go-v2/service/sts v1.16.9/go.mod h1:O1IvkYxr+39hRf960Us6j0x1P8pDqhTX+oXM5kQNl/Y=
github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM=
github.com/aws/smithy-go v1.12.0 h1:gXpeZel/jPoWQ7OEmLIgCUnhkFftqNfwWUwAHSlp1v0= github.com/aws/smithy-go v1.12.0 h1:gXpeZel/jPoWQ7OEmLIgCUnhkFftqNfwWUwAHSlp1v0=
github.com/aws/smithy-go v1.12.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/aws/smithy-go v1.12.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
@ -292,8 +292,8 @@ 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-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/gin-gonic/gin v1.7.3/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= github.com/gin-gonic/gin v1.7.3/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/go-acme/lego/v4 v4.7.0 h1:f5/9EigoIC3Lu14XUsbDzlJ1ZcTYBN3zu/0TJExQaOc= github.com/go-acme/lego/v4 v4.8.0 h1:XienkuT6ZKHe0DE/LXeGP4ZY+ft+7ZMlqtiJ7XJs2pI=
github.com/go-acme/lego/v4 v4.7.0/go.mod h1:hoPWeY+jooDbgbe5GUqHTGRGdENDhrkiypiDCAqgmmg= github.com/go-acme/lego/v4 v4.8.0/go.mod h1:MXCdgHuQh25bfi/tPpyOV/9k2p1JVu6oxXcylAwkouI=
github.com/go-chi/chi/v5 v5.0.4/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.4/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.0.8-0.20220512131524-9e71a0d4b3d6 h1:+fT7oFUOersdx+u7uIxOjabDVGxg+qqNV6kRdAXIvaQ= github.com/go-chi/chi/v5 v5.0.8-0.20220512131524-9e71a0d4b3d6 h1:+fT7oFUOersdx+u7uIxOjabDVGxg+qqNV6kRdAXIvaQ=
github.com/go-chi/chi/v5 v5.0.8-0.20220512131524-9e71a0d4b3d6/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.8-0.20220512131524-9e71a0d4b3d6/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
@ -710,8 +710,8 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo= github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
github.com/sftpgo/sdk v0.1.2-0.20220611083241-b653555f7f4d h1:gpshxOhLsGFbCy4ke96X8FVMy4xvXZQChSF7dikqxp4= github.com/sftpgo/sdk v0.1.2-0.20220611083241-b653555f7f4d h1:gpshxOhLsGFbCy4ke96X8FVMy4xvXZQChSF7dikqxp4=
github.com/sftpgo/sdk v0.1.2-0.20220611083241-b653555f7f4d/go.mod h1:JdxJrGnk6RKhRMTqwH5fFfaMiZuGi5qR1HxQaSDsswo= github.com/sftpgo/sdk v0.1.2-0.20220611083241-b653555f7f4d/go.mod h1:JdxJrGnk6RKhRMTqwH5fFfaMiZuGi5qR1HxQaSDsswo=
github.com/shirou/gopsutil/v3 v3.22.5 h1:atX36I/IXgFiB81687vSiBI5zrMsxcIBkP9cQMJQoJA= github.com/shirou/gopsutil/v3 v3.22.6 h1:FnHOFOh+cYAM0C30P+zysPISzlknLC5Z1G4EAElznfQ=
github.com/shirou/gopsutil/v3 v3.22.5/go.mod h1:so9G9VzeHt/hsd0YwqprnjHnfARAUktauykSbr+y2gA= github.com/shirou/gopsutil/v3 v3.22.6/go.mod h1:EdIubSnZhbAvBS1yJ7Xi+AShB/hxwLHOMz4MCYz7yMs=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
@ -744,6 +744,7 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/studio-b12/gowebdav v0.0.0-20220128162035-c7b1ff8a5e62 h1:b2nJXyPCa9HY7giGM+kYcnQ71m14JnGdQabMPmyt++8= github.com/studio-b12/gowebdav v0.0.0-20220128162035-c7b1ff8a5e62 h1:b2nJXyPCa9HY7giGM+kYcnQ71m14JnGdQabMPmyt++8=
@ -759,8 +760,8 @@ github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjM
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns= github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/unrolled/secure v1.10.0 h1:TBNP42z2AB+2pW9PR6vdbqhlQuv1iTeSVzK1qHjOBzA= github.com/unrolled/secure v1.11.0 h1:fjkKhD/MsQnlmz/au+MmFptCFNhvf5iv04ALkdCXRCI=
github.com/unrolled/secure v1.10.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= github.com/unrolled/secure v1.11.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I= github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I=
github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ= github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ=
github.com/xhit/go-simple-mail/v2 v2.11.0 h1:o/056V50zfkO3Mm5tVdo9rG3ryg4ZmJ2XW5GMinHfVs= github.com/xhit/go-simple-mail/v2 v2.11.0 h1:o/056V50zfkO3Mm5tVdo9rG3ryg4ZmJ2XW5GMinHfVs=
@ -970,8 +971,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b h1:2n253B2r0pYSmEV+UNCQoPfU/FiaizQEK5Gu4Bq4JE8= golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d h1:/m5NbqQelATgoSPVC2Z23sR4kVNokFwDDyWh/3rGY+I=
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -1221,8 +1222,8 @@ google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljW
google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220630160836-4327a74d660d h1:9IbUS1WUO1OPTBCp32JFwX9MqyT5pc3UWXH8plPX8is= google.golang.org/genproto v0.0.0-20220708155623-50e5f4832e73 h1:sdZWfcGN37Dv0QWIhuasQGMzAQJOL2oqnvot4/kPgfQ=
google.golang.org/genproto v0.0.0-20220630160836-4327a74d660d/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220708155623-50e5f4832e73/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=

235
httpd/api_eventrule.go Normal file
View file

@ -0,0 +1,235 @@
package httpd
import (
"context"
"net/http"
"github.com/go-chi/render"
"github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/util"
)
func getEventActions(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
limit, offset, order, err := getSearchFilters(w, r)
if err != nil {
return
}
actions, err := dataprovider.GetEventActions(limit, offset, order, false)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusInternalServerError)
return
}
render.JSON(w, r, actions)
}
func renderEventAction(w http.ResponseWriter, r *http.Request, name string, status int) {
action, err := dataprovider.EventActionExists(name)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
action.PrepareForRendering()
if status != http.StatusOK {
ctx := context.WithValue(r.Context(), render.StatusCtxKey, status)
render.JSON(w, r.WithContext(ctx), action)
} else {
render.JSON(w, r, action)
}
}
func getEventActionByName(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
name := getURLParam(r, "name")
renderEventAction(w, r, name, http.StatusOK)
}
func addEventAction(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
return
}
var action dataprovider.BaseEventAction
err = render.DecodeJSON(r.Body, &action)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
err = dataprovider.AddEventAction(&action, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
renderEventAction(w, r, action.Name, http.StatusCreated)
}
func updateEventAction(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
return
}
name := getURLParam(r, "name")
action, err := dataprovider.EventActionExists(name)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
actionID := action.ID
name = action.Name
currentHTTPPassword := action.Options.HTTPConfig.Password
action.Options = dataprovider.BaseEventActionOptions{}
err = render.DecodeJSON(r.Body, &action)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
action.ID = actionID
action.Name = name
action.Options.SetEmptySecretsIfNil()
switch action.Type {
case dataprovider.ActionTypeHTTP:
if action.Options.HTTPConfig.Password.IsNotPlainAndNotEmpty() {
action.Options.HTTPConfig.Password = currentHTTPPassword
}
}
err = dataprovider.UpdateEventAction(&action, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
sendAPIResponse(w, r, nil, "Event target updated", http.StatusOK)
}
func deleteEventAction(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
return
}
name := getURLParam(r, "name")
err = dataprovider.DeleteEventAction(name, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
sendAPIResponse(w, r, err, "Event action deleted", http.StatusOK)
}
func getEventRules(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
limit, offset, order, err := getSearchFilters(w, r)
if err != nil {
return
}
rules, err := dataprovider.GetEventRules(limit, offset, order)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusInternalServerError)
return
}
render.JSON(w, r, rules)
}
func renderEventRule(w http.ResponseWriter, r *http.Request, name string, status int) {
rule, err := dataprovider.EventRuleExists(name)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
rule.PrepareForRendering()
if status != http.StatusOK {
ctx := context.WithValue(r.Context(), render.StatusCtxKey, status)
render.JSON(w, r.WithContext(ctx), rule)
} else {
render.JSON(w, r, rule)
}
}
func getEventRuleByName(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
name := getURLParam(r, "name")
renderEventRule(w, r, name, http.StatusOK)
}
func addEventRule(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
return
}
var rule dataprovider.EventRule
err = render.DecodeJSON(r.Body, &rule)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
err = dataprovider.AddEventRule(&rule, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
renderEventRule(w, r, rule.Name, http.StatusCreated)
}
func updateEventRule(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
return
}
name := getURLParam(r, "name")
rule, err := dataprovider.EventRuleExists(name)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
ruleID := rule.ID
name = rule.Name
rule.Actions = nil
err = render.DecodeJSON(r.Body, &rule)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
rule.ID = ruleID
rule.Name = name
err = dataprovider.UpdateEventRule(&rule, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
sendAPIResponse(w, r, nil, "Event rules updated", http.StatusOK)
}
func deleteEventRule(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
return
}
name := getURLParam(r, "name")
err = dataprovider.DeleteEventRule(name, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
sendAPIResponse(w, r, err, "Event rule deleted", http.StatusOK)
}

View file

@ -13,7 +13,6 @@ import (
"github.com/go-chi/render" "github.com/go-chi/render"
"github.com/drakkan/sftpgo/v2/common"
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/util" "github.com/drakkan/sftpgo/v2/util"
@ -190,7 +189,15 @@ func restoreBackup(content []byte, inputFile string, scanQuota, mode int, execut
return err return err
} }
logger.Debug(logSender, "", "backup restored, users: %v, folders: %v, admins: %vs", if err = RestoreEventActions(dump.EventActions, inputFile, mode, executor, ipAddress); err != nil {
return err
}
if err = RestoreEventRules(dump.EventRules, inputFile, mode, executor, ipAddress); err != nil {
return err
}
logger.Debug(logSender, "", "backup restored, users: %d, folders: %d, admins: %d",
len(dump.Users), len(dump.Folders), len(dump.Admins)) len(dump.Users), len(dump.Folders), len(dump.Admins))
return nil return nil
@ -244,7 +251,7 @@ func RestoreFolders(folders []vfs.BaseVirtualFolder, inputFile string, mode, sca
return fmt.Errorf("unable to restore folder %#v: %w", folder.Name, err) return fmt.Errorf("unable to restore folder %#v: %w", folder.Name, err)
} }
if scanQuota >= 1 { if scanQuota >= 1 {
if common.QuotaScans.AddVFolderQuotaScan(folder.Name) { if dataprovider.QuotaScans.AddVFolderQuotaScan(folder.Name) {
logger.Debug(logSender, "", "starting quota scan for restored folder: %#v", folder.Name) logger.Debug(logSender, "", "starting quota scan for restored folder: %#v", folder.Name)
go doFolderQuotaScan(folder) //nolint:errcheck go doFolderQuotaScan(folder) //nolint:errcheck
} }
@ -280,6 +287,54 @@ func RestoreShares(shares []dataprovider.Share, inputFile string, mode int, exec
return nil return nil
} }
// RestoreEventActions restores the specified event actions
func RestoreEventActions(actions []dataprovider.BaseEventAction, inputFile string, mode int, executor, ipAddress string) error {
for _, action := range actions {
action := action // pin
a, err := dataprovider.EventActionExists(action.Name)
if err == nil {
if mode == 1 {
logger.Debug(logSender, "", "loaddata mode 1, existing event action %q not updated", a.Name)
continue
}
action.ID = a.ID
err = dataprovider.UpdateEventAction(&action, executor, ipAddress)
logger.Debug(logSender, "", "restoring event action %q, dump file: %q, error: %v", action.Name, inputFile, err)
} else {
err = dataprovider.AddEventAction(&action, executor, ipAddress)
logger.Debug(logSender, "", "adding new event action %q, dump file: %q, error: %v", action.Name, inputFile, err)
}
if err != nil {
return fmt.Errorf("unable to restore event action %q: %w", action.Name, err)
}
}
return nil
}
// RestoreEventRules restores the specified event rules
func RestoreEventRules(rules []dataprovider.EventRule, inputFile string, mode int, executor, ipAddress string) error {
for _, rule := range rules {
rule := rule // pin
r, err := dataprovider.EventRuleExists(rule.Name)
if err == nil {
if mode == 1 {
logger.Debug(logSender, "", "loaddata mode 1, existing event rule %q not updated", r.Name)
continue
}
rule.ID = r.ID
err = dataprovider.UpdateEventRule(&rule, executor, ipAddress)
logger.Debug(logSender, "", "restoring event rule %q, dump file: %q, error: %v", rule.Name, inputFile, err)
} else {
err = dataprovider.AddEventRule(&rule, executor, ipAddress)
logger.Debug(logSender, "", "adding new event rule %q, dump file: %q, error: %v", rule.Name, inputFile, err)
}
if err != nil {
return fmt.Errorf("unable to restore event rule %q: %w", rule.Name, err)
}
}
return nil
}
// RestoreAPIKeys restores the specified API keys // RestoreAPIKeys restores the specified API keys
func RestoreAPIKeys(apiKeys []dataprovider.APIKey, inputFile string, mode int, executor, ipAddress string) error { func RestoreAPIKeys(apiKeys []dataprovider.APIKey, inputFile string, mode int, executor, ipAddress string) error {
for _, apiKey := range apiKeys { for _, apiKey := range apiKeys {
@ -384,7 +439,7 @@ func RestoreUsers(users []dataprovider.User, inputFile string, mode, scanQuota i
return fmt.Errorf("unable to restore user %#v: %w", user.Username, err) return fmt.Errorf("unable to restore user %#v: %w", user.Username, err)
} }
if scanQuota == 1 || (scanQuota == 2 && user.HasQuotaRestrictions()) { if scanQuota == 1 || (scanQuota == 2 && user.HasQuotaRestrictions()) {
if common.QuotaScans.AddUserQuotaScan(user.Username) { if dataprovider.QuotaScans.AddUserQuotaScan(user.Username) {
logger.Debug(logSender, "", "starting quota scan for restored user: %#v", user.Username) logger.Debug(logSender, "", "starting quota scan for restored user: %#v", user.Username)
go doUserQuotaScan(user) //nolint:errcheck go doUserQuotaScan(user) //nolint:errcheck
} }

View file

@ -7,7 +7,6 @@ import (
"github.com/go-chi/render" "github.com/go-chi/render"
"github.com/drakkan/sftpgo/v2/common"
"github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/logger" "github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/vfs" "github.com/drakkan/sftpgo/v2/vfs"
@ -30,12 +29,12 @@ type transferQuotaUsage struct {
func getUsersQuotaScans(w http.ResponseWriter, r *http.Request) { func getUsersQuotaScans(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
render.JSON(w, r, common.QuotaScans.GetUsersQuotaScans()) render.JSON(w, r, dataprovider.QuotaScans.GetUsersQuotaScans())
} }
func getFoldersQuotaScans(w http.ResponseWriter, r *http.Request) { func getFoldersQuotaScans(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
render.JSON(w, r, common.QuotaScans.GetVFoldersQuotaScans()) render.JSON(w, r, dataprovider.QuotaScans.GetVFoldersQuotaScans())
} }
func updateUserQuotaUsage(w http.ResponseWriter, r *http.Request) { func updateUserQuotaUsage(w http.ResponseWriter, r *http.Request) {
@ -128,11 +127,11 @@ func doUpdateUserQuotaUsage(w http.ResponseWriter, r *http.Request, username str
"", http.StatusBadRequest) "", http.StatusBadRequest)
return return
} }
if !common.QuotaScans.AddUserQuotaScan(user.Username) { if !dataprovider.QuotaScans.AddUserQuotaScan(user.Username) {
sendAPIResponse(w, r, err, "A quota scan is in progress for this user", http.StatusConflict) sendAPIResponse(w, r, err, "A quota scan is in progress for this user", http.StatusConflict)
return return
} }
defer common.QuotaScans.RemoveUserQuotaScan(user.Username) defer dataprovider.QuotaScans.RemoveUserQuotaScan(user.Username)
err = dataprovider.UpdateUserQuota(&user, usage.UsedQuotaFiles, usage.UsedQuotaSize, mode == quotaUpdateModeReset) err = dataprovider.UpdateUserQuota(&user, usage.UsedQuotaFiles, usage.UsedQuotaSize, mode == quotaUpdateModeReset)
if err != nil { if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err)) sendAPIResponse(w, r, err, "", getRespStatus(err))
@ -157,11 +156,11 @@ func doUpdateFolderQuotaUsage(w http.ResponseWriter, r *http.Request, name strin
sendAPIResponse(w, r, err, "", getRespStatus(err)) sendAPIResponse(w, r, err, "", getRespStatus(err))
return return
} }
if !common.QuotaScans.AddVFolderQuotaScan(folder.Name) { if !dataprovider.QuotaScans.AddVFolderQuotaScan(folder.Name) {
sendAPIResponse(w, r, err, "A quota scan is in progress for this folder", http.StatusConflict) sendAPIResponse(w, r, err, "A quota scan is in progress for this folder", http.StatusConflict)
return return
} }
defer common.QuotaScans.RemoveVFolderQuotaScan(folder.Name) defer dataprovider.QuotaScans.RemoveVFolderQuotaScan(folder.Name)
err = dataprovider.UpdateVirtualFolderQuota(&folder, usage.UsedQuotaFiles, usage.UsedQuotaSize, mode == quotaUpdateModeReset) err = dataprovider.UpdateVirtualFolderQuota(&folder, usage.UsedQuotaFiles, usage.UsedQuotaSize, mode == quotaUpdateModeReset)
if err != nil { if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err)) sendAPIResponse(w, r, err, "", getRespStatus(err))
@ -180,8 +179,8 @@ func doStartUserQuotaScan(w http.ResponseWriter, r *http.Request, username strin
sendAPIResponse(w, r, err, "", getRespStatus(err)) sendAPIResponse(w, r, err, "", getRespStatus(err))
return return
} }
if !common.QuotaScans.AddUserQuotaScan(user.Username) { if !dataprovider.QuotaScans.AddUserQuotaScan(user.Username) {
sendAPIResponse(w, r, err, fmt.Sprintf("Another scan is already in progress for user %#v", username), sendAPIResponse(w, r, nil, fmt.Sprintf("Another scan is already in progress for user %#v", username),
http.StatusConflict) http.StatusConflict)
return return
} }
@ -199,7 +198,7 @@ func doStartFolderQuotaScan(w http.ResponseWriter, r *http.Request, name string)
sendAPIResponse(w, r, err, "", getRespStatus(err)) sendAPIResponse(w, r, err, "", getRespStatus(err))
return return
} }
if !common.QuotaScans.AddVFolderQuotaScan(folder.Name) { if !dataprovider.QuotaScans.AddVFolderQuotaScan(folder.Name) {
sendAPIResponse(w, r, err, fmt.Sprintf("Another scan is already in progress for folder %#v", name), sendAPIResponse(w, r, err, fmt.Sprintf("Another scan is already in progress for folder %#v", name),
http.StatusConflict) http.StatusConflict)
return return
@ -209,7 +208,7 @@ func doStartFolderQuotaScan(w http.ResponseWriter, r *http.Request, name string)
} }
func doUserQuotaScan(user dataprovider.User) error { func doUserQuotaScan(user dataprovider.User) error {
defer common.QuotaScans.RemoveUserQuotaScan(user.Username) defer dataprovider.QuotaScans.RemoveUserQuotaScan(user.Username)
numFiles, size, err := user.ScanQuota() numFiles, size, err := user.ScanQuota()
if err != nil { if err != nil {
logger.Warn(logSender, "", "error scanning user quota %#v: %v", user.Username, err) logger.Warn(logSender, "", "error scanning user quota %#v: %v", user.Username, err)
@ -221,7 +220,7 @@ func doUserQuotaScan(user dataprovider.User) error {
} }
func doFolderQuotaScan(folder vfs.BaseVirtualFolder) error { func doFolderQuotaScan(folder vfs.BaseVirtualFolder) error {
defer common.QuotaScans.RemoveVFolderQuotaScan(folder.Name) defer dataprovider.QuotaScans.RemoveVFolderQuotaScan(folder.Name)
f := vfs.VirtualFolder{ f := vfs.VirtualFolder{
BaseVirtualFolder: folder, BaseVirtualFolder: folder,
VirtualPath: "/", VirtualPath: "/",

View file

@ -574,7 +574,7 @@ func handleForgotPassword(r *http.Request, username string, isAdmin bool) error
return util.NewGenericError("Unable to render password reset template") return util.NewGenericError("Unable to render password reset template")
} }
startTime := time.Now() startTime := time.Now()
if err := smtp.SendEmail(email, subject, body.String(), smtp.EmailContentTypeTextHTML); err != nil { if err := smtp.SendEmail([]string{email}, subject, body.String(), smtp.EmailContentTypeTextHTML); err != nil {
logger.Warn(logSender, middleware.GetReqID(r.Context()), "unable to send password reset code via email: %v, elapsed: %v", logger.Warn(logSender, middleware.GetReqID(r.Context()), "unable to send password reset code via email: %v, elapsed: %v",
err, time.Since(startTime)) err, time.Since(startTime))
return util.NewGenericError(fmt.Sprintf("Unable to send confirmation code via email: %v", err)) return util.NewGenericError(fmt.Sprintf("Unable to send confirmation code via email: %v", err))

View file

@ -74,6 +74,8 @@ const (
fsEventsPath = "/api/v2/events/fs" fsEventsPath = "/api/v2/events/fs"
providerEventsPath = "/api/v2/events/provider" providerEventsPath = "/api/v2/events/provider"
sharesPath = "/api/v2/shares" sharesPath = "/api/v2/shares"
eventActionsPath = "/api/v2/eventactions"
eventRulesPath = "/api/v2/eventrules"
healthzPath = "/healthz" healthzPath = "/healthz"
robotsTxtPath = "/robots.txt" robotsTxtPath = "/robots.txt"
webRootPathDefault = "/" webRootPathDefault = "/"
@ -107,6 +109,10 @@ const (
webAdminResetPwdPathDefault = "/web/admin/reset-password" webAdminResetPwdPathDefault = "/web/admin/reset-password"
webAdminProfilePathDefault = "/web/admin/profile" webAdminProfilePathDefault = "/web/admin/profile"
webAdminMFAPathDefault = "/web/admin/mfa" webAdminMFAPathDefault = "/web/admin/mfa"
webAdminEventRulesPathDefault = "/web/admin/eventrules"
webAdminEventRulePathDefault = "/web/admin/eventrule"
webAdminEventActionsPathDefault = "/web/admin/eventactions"
webAdminEventActionPathDefault = "/web/admin/eventaction"
webAdminTOTPGeneratePathDefault = "/web/admin/totp/generate" webAdminTOTPGeneratePathDefault = "/web/admin/totp/generate"
webAdminTOTPValidatePathDefault = "/web/admin/totp/validate" webAdminTOTPValidatePathDefault = "/web/admin/totp/validate"
webAdminTOTPSavePathDefault = "/web/admin/totp/save" webAdminTOTPSavePathDefault = "/web/admin/totp/save"
@ -185,6 +191,10 @@ var (
webQuotaScanPath string webQuotaScanPath string
webAdminProfilePath string webAdminProfilePath string
webAdminMFAPath string webAdminMFAPath string
webAdminEventRulesPath string
webAdminEventRulePath string
webAdminEventActionsPath string
webAdminEventActionPath string
webAdminTOTPGeneratePath string webAdminTOTPGeneratePath string
webAdminTOTPValidatePath string webAdminTOTPValidatePath string
webAdminTOTPSavePath string webAdminTOTPSavePath string
@ -755,6 +765,7 @@ func (c *Conf) Initialize(configDir string, isShared int) error {
return return
} }
server := newHttpdServer(b, staticFilesPath, c.SigningPassphrase, c.Cors, openAPIPath) server := newHttpdServer(b, staticFilesPath, c.SigningPassphrase, c.Cors, openAPIPath)
server.setShared(isShared)
exitChannel <- server.listenAndServe() exitChannel <- server.listenAndServe()
}(binding) }(binding)
@ -890,6 +901,10 @@ func updateWebAdminURLs(baseURL string) {
webAdminResetPwdPath = path.Join(baseURL, webAdminResetPwdPathDefault) webAdminResetPwdPath = path.Join(baseURL, webAdminResetPwdPathDefault)
webAdminProfilePath = path.Join(baseURL, webAdminProfilePathDefault) webAdminProfilePath = path.Join(baseURL, webAdminProfilePathDefault)
webAdminMFAPath = path.Join(baseURL, webAdminMFAPathDefault) webAdminMFAPath = path.Join(baseURL, webAdminMFAPathDefault)
webAdminEventRulesPath = path.Join(baseURL, webAdminEventRulesPathDefault)
webAdminEventRulePath = path.Join(baseURL, webAdminEventRulePathDefault)
webAdminEventActionsPath = path.Join(baseURL, webAdminEventActionsPathDefault)
webAdminEventActionPath = path.Join(baseURL, webAdminEventActionPathDefault)
webAdminTOTPGeneratePath = path.Join(baseURL, webAdminTOTPGeneratePathDefault) webAdminTOTPGeneratePath = path.Join(baseURL, webAdminTOTPGeneratePathDefault)
webAdminTOTPValidatePath = path.Join(baseURL, webAdminTOTPValidatePathDefault) webAdminTOTPValidatePath = path.Join(baseURL, webAdminTOTPValidatePathDefault)
webAdminTOTPSavePath = path.Join(baseURL, webAdminTOTPSavePathDefault) webAdminTOTPSavePath = path.Join(baseURL, webAdminTOTPSavePathDefault)

File diff suppressed because it is too large Load diff

View file

@ -611,6 +611,36 @@ func TestInvalidToken(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, rr.Code) assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims") assert.Contains(t, rr.Body.String(), "Invalid token claims")
rr = httptest.NewRecorder()
addEventAction(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims")
rr = httptest.NewRecorder()
updateEventAction(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims")
rr = httptest.NewRecorder()
deleteEventAction(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims")
rr = httptest.NewRecorder()
addEventRule(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims")
rr = httptest.NewRecorder()
updateEventRule(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims")
rr = httptest.NewRecorder()
deleteEventRule(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims")
rr = httptest.NewRecorder() rr = httptest.NewRecorder()
server.handleWebAddAdminPost(rr, req) server.handleWebAddAdminPost(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code) assert.Equal(t, http.StatusBadRequest, rr.Code)
@ -626,6 +656,26 @@ func TestInvalidToken(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, rr.Code) assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "invalid token claims") assert.Contains(t, rr.Body.String(), "invalid token claims")
rr = httptest.NewRecorder()
server.handleWebAddEventActionPost(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "invalid token claims")
rr = httptest.NewRecorder()
server.handleWebUpdateEventActionPost(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "invalid token claims")
rr = httptest.NewRecorder()
server.handleWebAddEventRulePost(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "invalid token claims")
rr = httptest.NewRecorder()
server.handleWebUpdateEventRulePost(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "invalid token claims")
rr = httptest.NewRecorder() rr = httptest.NewRecorder()
server.handleWebClientTwoFactorRecoveryPost(rr, req) server.handleWebClientTwoFactorRecoveryPost(rr, req)
assert.Equal(t, http.StatusNotFound, rr.Code) assert.Equal(t, http.StatusNotFound, rr.Code)
@ -836,6 +886,7 @@ func TestCreateTokenError(t *testing.T) {
rr = httptest.NewRecorder() rr = httptest.NewRecorder()
server.handleWebAdminLoginPost(rr, req) server.handleWebAdminLoginPost(rr, req)
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
req, _ = http.NewRequest(http.MethodGet, webAdminLoginPath+"?a=a%C3%A1%G2", nil) req, _ = http.NewRequest(http.MethodGet, webAdminLoginPath+"?a=a%C3%A1%G2", nil)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = httptest.NewRecorder() rr = httptest.NewRecorder()
@ -848,6 +899,16 @@ func TestCreateTokenError(t *testing.T) {
_, err := getAdminFromPostFields(req) _, err := getAdminFromPostFields(req)
assert.Error(t, err) assert.Error(t, err)
req, _ = http.NewRequest(http.MethodPost, webAdminEventActionPath+"?a=a%C3%AO%GG", nil)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
_, err = getEventActionFromPostFields(req)
assert.Error(t, err)
req, _ = http.NewRequest(http.MethodPost, webAdminEventRulePath+"?a=a%C3%AO%GG", nil)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
_, err = getEventRuleFromPostFields(req)
assert.Error(t, err)
req, _ = http.NewRequest(http.MethodPost, webClientLoginPath+"?a=a%C3%AO%GG", bytes.NewBuffer([]byte(form.Encode()))) req, _ = http.NewRequest(http.MethodPost, webClientLoginPath+"?a=a%C3%AO%GG", bytes.NewBuffer([]byte(form.Encode())))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = httptest.NewRecorder() rr = httptest.NewRecorder()
@ -1330,7 +1391,7 @@ func TestQuotaScanInvalidFs(t *testing.T) {
Provider: sdk.S3FilesystemProvider, Provider: sdk.S3FilesystemProvider,
}, },
} }
common.QuotaScans.AddUserQuotaScan(user.Username) dataprovider.QuotaScans.AddUserQuotaScan(user.Username)
err := doUserQuotaScan(user) err := doUserQuotaScan(user)
assert.Error(t, err) assert.Error(t, err)
} }

View file

@ -44,6 +44,7 @@ type httpdServer struct {
enableWebAdmin bool enableWebAdmin bool
enableWebClient bool enableWebClient bool
renderOpenAPI bool renderOpenAPI bool
isShared int
router *chi.Mux router *chi.Mux
tokenAuth *jwtauth.JWTAuth tokenAuth *jwtauth.JWTAuth
signingPassphrase string signingPassphrase string
@ -68,6 +69,10 @@ func newHttpdServer(b Binding, staticFilesPath, signingPassphrase string, cors C
} }
} }
func (s *httpdServer) setShared(value int) {
s.isShared = value
}
func (s *httpdServer) listenAndServe() error { func (s *httpdServer) listenAndServe() error {
s.initializeRouter() s.initializeRouter()
httpServer := &http.Server{ httpServer := &http.Server{
@ -1259,6 +1264,16 @@ func (s *httpdServer) initializeRouter() {
Put(apiKeysPath+"/{id}", updateAPIKey) Put(apiKeysPath+"/{id}", updateAPIKey)
router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)). router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
Delete(apiKeysPath+"/{id}", deleteAPIKey) Delete(apiKeysPath+"/{id}", deleteAPIKey)
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Get(eventActionsPath, getEventActions)
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Get(eventActionsPath+"/{name}", getEventActionByName)
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(eventActionsPath, addEventAction)
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Put(eventActionsPath+"/{name}", updateEventAction)
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Delete(eventActionsPath+"/{name}", deleteEventAction)
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Get(eventRulesPath, getEventRules)
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Get(eventRulesPath+"/{name}", getEventRuleByName)
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(eventRulesPath, addEventRule)
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Put(eventRulesPath+"/{name}", updateEventRule)
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Delete(eventRulesPath+"/{name}", deleteEventRule)
}) })
s.router.Get(userTokenPath, s.getUserToken) s.router.Get(userTokenPath, s.getUserToken)
@ -1504,7 +1519,8 @@ func (s *httpdServer) setupWebAdminRoutes() {
router.With(verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminTOTPGeneratePath, generateTOTPSecret) router.With(verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminTOTPGeneratePath, generateTOTPSecret)
router.With(verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminTOTPValidatePath, validateTOTPPasscode) router.With(verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminTOTPValidatePath, validateTOTPPasscode)
router.With(verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminTOTPSavePath, saveTOTPConfig) router.With(verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminTOTPSavePath, saveTOTPConfig)
router.With(verifyCSRFHeader, s.requireBuiltinLogin, s.refreshCookie).Get(webAdminRecoveryCodesPath, getRecoveryCodes) router.With(verifyCSRFHeader, s.requireBuiltinLogin, s.refreshCookie).Get(webAdminRecoveryCodesPath,
getRecoveryCodes)
router.With(verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminRecoveryCodesPath, generateRecoveryCodes) router.With(verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminRecoveryCodesPath, generateRecoveryCodes)
router.With(s.checkPerm(dataprovider.PermAdminViewUsers), s.refreshCookie). router.With(s.checkPerm(dataprovider.PermAdminViewUsers), s.refreshCookie).
@ -1574,6 +1590,30 @@ func (s *httpdServer) setupWebAdminRoutes() {
router.With(s.checkPerm(dataprovider.PermAdminViewDefender)).Get(webDefenderHostsPath, getDefenderHosts) router.With(s.checkPerm(dataprovider.PermAdminViewDefender)).Get(webDefenderHostsPath, getDefenderHosts)
router.With(s.checkPerm(dataprovider.PermAdminManageDefender)).Delete(webDefenderHostsPath+"/{id}", router.With(s.checkPerm(dataprovider.PermAdminManageDefender)).Delete(webDefenderHostsPath+"/{id}",
deleteDefenderHostByID) deleteDefenderHostByID)
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie).
Get(webAdminEventActionsPath, s.handleWebGetEventActions)
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie).
Get(webAdminEventActionPath, s.handleWebAddEventActionGet)
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(webAdminEventActionPath,
s.handleWebAddEventActionPost)
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie).
Get(webAdminEventActionPath+"/{name}", s.handleWebUpdateEventActionGet)
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(webAdminEventActionPath+"/{name}",
s.handleWebUpdateEventActionPost)
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), verifyCSRFHeader).
Delete(webAdminEventActionPath+"/{name}", deleteEventAction)
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie).
Get(webAdminEventRulesPath, s.handleWebGetEventRules)
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie).
Get(webAdminEventRulePath, s.handleWebAddEventRuleGet)
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(webAdminEventRulePath,
s.handleWebAddEventRulePost)
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie).
Get(webAdminEventRulePath+"/{name}", s.handleWebUpdateEventRuleGet)
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(webAdminEventRulePath+"/{name}",
s.handleWebUpdateEventRulePost)
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), verifyCSRFHeader).
Delete(webAdminEventRulePath+"/{name}", deleteEventRule)
}) })
} }
} }

View file

@ -43,11 +43,11 @@ const (
folderPageModeTemplate folderPageModeTemplate
) )
type groupPageMode int type genericPageMode int
const ( const (
groupPageModeAdd groupPageMode = iota + 1 genericPageModeAdd genericPageMode = iota + 1
groupPageModeUpdate genericPageModeUpdate
) )
const ( const (
@ -65,6 +65,10 @@ const (
templateGroup = "group.html" templateGroup = "group.html"
templateFolders = "folders.html" templateFolders = "folders.html"
templateFolder = "folder.html" templateFolder = "folder.html"
templateEventRules = "eventrules.html"
templateEventRule = "eventrule.html"
templateEventActions = "eventactions.html"
templateEventAction = "eventaction.html"
templateMessage = "message.html" templateMessage = "message.html"
templateStatus = "status.html" templateStatus = "status.html"
templateLogin = "login.html" templateLogin = "login.html"
@ -80,6 +84,8 @@ const (
pageStatusTitle = "Status" pageStatusTitle = "Status"
pageFoldersTitle = "Folders" pageFoldersTitle = "Folders"
pageGroupsTitle = "Groups" pageGroupsTitle = "Groups"
pageEventRulesTitle = "Event rules"
pageEventActionsTitle = "Event actions"
pageProfileTitle = "My profile" pageProfileTitle = "My profile"
pageChangePwdTitle = "Change password" pageChangePwdTitle = "Change password"
pageMaintenanceTitle = "Maintenance" pageMaintenanceTitle = "Maintenance"
@ -114,6 +120,10 @@ type basePage struct {
ProfileURL string ProfileURL string
ChangePwdURL string ChangePwdURL string
MFAURL string MFAURL string
EventRulesURL string
EventRuleURL string
EventActionsURL string
EventActionURL string
FolderQuotaScanURL string FolderQuotaScanURL string
StatusURL string StatusURL string
MaintenanceURL string MaintenanceURL string
@ -123,11 +133,14 @@ type basePage struct {
ConnectionsTitle string ConnectionsTitle string
FoldersTitle string FoldersTitle string
GroupsTitle string GroupsTitle string
EventRulesTitle string
EventActionsTitle string
StatusTitle string StatusTitle string
MaintenanceTitle string MaintenanceTitle string
DefenderTitle string DefenderTitle string
Version string Version string
CSRFToken string CSRFToken string
IsEventManagerPage bool
HasDefender bool HasDefender bool
HasExternalLogin bool HasExternalLogin bool
LoggedAdmin *dataprovider.Admin LoggedAdmin *dataprovider.Admin
@ -154,6 +167,16 @@ type groupsPage struct {
Groups []dataprovider.Group Groups []dataprovider.Group
} }
type eventRulesPage struct {
basePage
Rules []dataprovider.EventRule
}
type eventActionsPage struct {
basePage
Actions []dataprovider.BaseEventAction
}
type connectionsPage struct { type connectionsPage struct {
basePage basePage
Connections []common.ConnectionStatus Connections []common.ConnectionStatus
@ -183,7 +206,6 @@ type userPage struct {
TwoFactorProtocols []string TwoFactorProtocols []string
WebClientOptions []string WebClientOptions []string
RootDirPerms []string RootDirPerms []string
RedactedSecret string
Mode userPageMode Mode userPageMode
VirtualFolders []vfs.BaseVirtualFolder VirtualFolders []vfs.BaseVirtualFolder
Groups []dataprovider.Group Groups []dataprovider.Group
@ -253,7 +275,7 @@ type groupPage struct {
basePage basePage
Group *dataprovider.Group Group *dataprovider.Group
Error string Error string
Mode groupPageMode Mode genericPageMode
ValidPerms []string ValidPerms []string
ValidLoginMethods []string ValidLoginMethods []string
ValidProtocols []string ValidProtocols []string
@ -263,6 +285,30 @@ type groupPage struct {
FsWrapper fsWrapper FsWrapper fsWrapper
} }
type eventActionPage struct {
basePage
Action dataprovider.BaseEventAction
ActionTypes []dataprovider.EnumMapping
HTTPMethods []string
RedactedSecret string
Error string
Mode genericPageMode
}
type eventRulePage struct {
basePage
Rule dataprovider.EventRule
TriggerTypes []dataprovider.EnumMapping
Actions []dataprovider.BaseEventAction
FsEvents []string
Protocols []string
ProviderEvents []string
ProviderObjects []string
Error string
Mode genericPageMode
IsShared bool
}
type messagePage struct { type messagePage struct {
basePage basePage
Error string Error string
@ -341,6 +387,26 @@ func loadAdminTemplates(templatesPath string) {
filepath.Join(templatesPath, templateAdminDir, templateSharedComponents), filepath.Join(templatesPath, templateAdminDir, templateSharedComponents),
filepath.Join(templatesPath, templateAdminDir, templateGroup), filepath.Join(templatesPath, templateAdminDir, templateGroup),
} }
eventRulesPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateEventRules),
}
eventRulePaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateEventRule),
}
eventActionsPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateEventActions),
}
eventActionPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateEventAction),
}
statusPaths := []string{ statusPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS), filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
filepath.Join(templatesPath, templateAdminDir, templateBase), filepath.Join(templatesPath, templateAdminDir, templateBase),
@ -408,6 +474,10 @@ func loadAdminTemplates(templatesPath string) {
groupTmpl := util.LoadTemplate(fsBaseTpl, groupPaths...) groupTmpl := util.LoadTemplate(fsBaseTpl, groupPaths...)
foldersTmpl := util.LoadTemplate(nil, foldersPaths...) foldersTmpl := util.LoadTemplate(nil, foldersPaths...)
folderTmpl := util.LoadTemplate(fsBaseTpl, folderPaths...) folderTmpl := util.LoadTemplate(fsBaseTpl, folderPaths...)
eventRulesTmpl := util.LoadTemplate(nil, eventRulesPaths...)
eventRuleTmpl := util.LoadTemplate(nil, eventRulePaths...)
eventActionsTmpl := util.LoadTemplate(nil, eventActionsPaths...)
eventActionTmpl := util.LoadTemplate(nil, eventActionPaths...)
statusTmpl := util.LoadTemplate(nil, statusPaths...) statusTmpl := util.LoadTemplate(nil, statusPaths...)
loginTmpl := util.LoadTemplate(nil, loginPaths...) loginTmpl := util.LoadTemplate(nil, loginPaths...)
profileTmpl := util.LoadTemplate(nil, profilePaths...) profileTmpl := util.LoadTemplate(nil, profilePaths...)
@ -431,6 +501,10 @@ func loadAdminTemplates(templatesPath string) {
adminTemplates[templateGroup] = groupTmpl adminTemplates[templateGroup] = groupTmpl
adminTemplates[templateFolders] = foldersTmpl adminTemplates[templateFolders] = foldersTmpl
adminTemplates[templateFolder] = folderTmpl adminTemplates[templateFolder] = folderTmpl
adminTemplates[templateEventRules] = eventRulesTmpl
adminTemplates[templateEventRule] = eventRuleTmpl
adminTemplates[templateEventActions] = eventActionsTmpl
adminTemplates[templateEventAction] = eventActionTmpl
adminTemplates[templateStatus] = statusTmpl adminTemplates[templateStatus] = statusTmpl
adminTemplates[templateLogin] = loginTmpl adminTemplates[templateLogin] = loginTmpl
adminTemplates[templateProfile] = profileTmpl adminTemplates[templateProfile] = profileTmpl
@ -445,6 +519,22 @@ func loadAdminTemplates(templatesPath string) {
adminTemplates[templateResetPassword] = resetPwdTmpl adminTemplates[templateResetPassword] = resetPwdTmpl
} }
func isEventManagerResource(currentURL string) bool {
if currentURL == webAdminEventRulesPath {
return true
}
if currentURL == webAdminEventActionsPath {
return true
}
if currentURL == webAdminEventRulePath || strings.HasPrefix(currentURL, webAdminEventRulePath+"/") {
return true
}
if currentURL == webAdminEventActionPath || strings.HasPrefix(currentURL, webAdminEventActionPath+"/") {
return true
}
return false
}
func (s *httpdServer) getBasePageData(title, currentURL string, r *http.Request) basePage { func (s *httpdServer) getBasePageData(title, currentURL string, r *http.Request) basePage {
var csrfToken string var csrfToken string
if currentURL != "" { if currentURL != "" {
@ -468,6 +558,10 @@ func (s *httpdServer) getBasePageData(title, currentURL string, r *http.Request)
ProfileURL: webAdminProfilePath, ProfileURL: webAdminProfilePath,
ChangePwdURL: webChangeAdminPwdPath, ChangePwdURL: webChangeAdminPwdPath,
MFAURL: webAdminMFAPath, MFAURL: webAdminMFAPath,
EventRulesURL: webAdminEventRulesPath,
EventRuleURL: webAdminEventRulePath,
EventActionsURL: webAdminEventActionsPath,
EventActionURL: webAdminEventActionPath,
QuotaScanURL: webQuotaScanPath, QuotaScanURL: webQuotaScanPath,
ConnectionsURL: webConnectionsPath, ConnectionsURL: webConnectionsPath,
StatusURL: webStatusPath, StatusURL: webStatusPath,
@ -479,11 +573,14 @@ func (s *httpdServer) getBasePageData(title, currentURL string, r *http.Request)
ConnectionsTitle: pageConnectionsTitle, ConnectionsTitle: pageConnectionsTitle,
FoldersTitle: pageFoldersTitle, FoldersTitle: pageFoldersTitle,
GroupsTitle: pageGroupsTitle, GroupsTitle: pageGroupsTitle,
EventRulesTitle: pageEventRulesTitle,
EventActionsTitle: pageEventActionsTitle,
StatusTitle: pageStatusTitle, StatusTitle: pageStatusTitle,
MaintenanceTitle: pageMaintenanceTitle, MaintenanceTitle: pageMaintenanceTitle,
DefenderTitle: pageDefenderTitle, DefenderTitle: pageDefenderTitle,
Version: version.GetAsString(), Version: version.GetAsString(),
LoggedAdmin: getAdminFromToken(r), LoggedAdmin: getAdminFromToken(r),
IsEventManagerPage: isEventManagerResource(currentURL),
HasDefender: common.Config.DefenderConfig.Enabled, HasDefender: common.Config.DefenderConfig.Enabled,
HasExternalLogin: isLoggedInWithOIDC(r), HasExternalLogin: isLoggedInWithOIDC(r),
CSRFToken: csrfToken, CSRFToken: csrfToken,
@ -721,7 +818,7 @@ func (s *httpdServer) renderUserPage(w http.ResponseWriter, r *http.Request, use
} }
func (s *httpdServer) renderGroupPage(w http.ResponseWriter, r *http.Request, group dataprovider.Group, func (s *httpdServer) renderGroupPage(w http.ResponseWriter, r *http.Request, group dataprovider.Group,
mode groupPageMode, error string, mode genericPageMode, error string,
) { ) {
folders, err := s.getWebVirtualFolders(w, r, defaultQueryLimit, true) folders, err := s.getWebVirtualFolders(w, r, defaultQueryLimit, true)
if err != nil { if err != nil {
@ -731,10 +828,10 @@ func (s *httpdServer) renderGroupPage(w http.ResponseWriter, r *http.Request, gr
group.UserSettings.FsConfig.RedactedSecret = redactedSecret group.UserSettings.FsConfig.RedactedSecret = redactedSecret
var title, currentURL string var title, currentURL string
switch mode { switch mode {
case groupPageModeAdd: case genericPageModeAdd:
title = "Add a new group" title = "Add a new group"
currentURL = webGroupPath currentURL = webGroupPath
case groupPageModeUpdate: case genericPageModeUpdate:
title = "Update group" title = "Update group"
currentURL = fmt.Sprintf("%v/%v", webGroupPath, url.PathEscape(group.Name)) currentURL = fmt.Sprintf("%v/%v", webGroupPath, url.PathEscape(group.Name))
} }
@ -763,6 +860,71 @@ func (s *httpdServer) renderGroupPage(w http.ResponseWriter, r *http.Request, gr
renderAdminTemplate(w, templateGroup, data) renderAdminTemplate(w, templateGroup, data)
} }
func (s *httpdServer) renderEventActionPage(w http.ResponseWriter, r *http.Request, action dataprovider.BaseEventAction,
mode genericPageMode, error string,
) {
action.Options.SetEmptySecretsIfNil()
var title, currentURL string
switch mode {
case genericPageModeAdd:
title = "Add a new event action"
currentURL = webAdminEventActionPath
case genericPageModeUpdate:
title = "Update event action"
currentURL = fmt.Sprintf("%v/%v", webAdminEventActionPath, url.PathEscape(action.Name))
}
if action.Options.HTTPConfig.Timeout == 0 {
action.Options.HTTPConfig.Timeout = 20
}
if action.Options.CmdConfig.Timeout == 0 {
action.Options.CmdConfig.Timeout = 20
}
data := eventActionPage{
basePage: s.getBasePageData(title, currentURL, r),
Action: action,
ActionTypes: dataprovider.EventActionTypes,
HTTPMethods: dataprovider.SupportedHTTPActionMethods,
RedactedSecret: redactedSecret,
Error: error,
Mode: mode,
}
renderAdminTemplate(w, templateEventAction, data)
}
func (s *httpdServer) renderEventRulePage(w http.ResponseWriter, r *http.Request, rule dataprovider.EventRule,
mode genericPageMode, error string,
) {
actions, err := s.getWebEventActions(w, r, defaultQueryLimit, true)
if err != nil {
return
}
var title, currentURL string
switch mode {
case genericPageModeAdd:
title = "Add new event rules"
currentURL = webAdminEventRulePath
case genericPageModeUpdate:
title = "Update event rules"
currentURL = fmt.Sprintf("%v/%v", webAdminEventRulePath, url.PathEscape(rule.Name))
}
data := eventRulePage{
basePage: s.getBasePageData(title, currentURL, r),
Rule: rule,
TriggerTypes: dataprovider.EventTriggerTypes,
Actions: actions,
FsEvents: dataprovider.SupportedFsEvents,
Protocols: dataprovider.SupportedRuleConditionProtocols,
ProviderEvents: dataprovider.SupportedProviderEvents,
ProviderObjects: dataprovider.SupporteRuleConditionProviderObjects,
Error: error,
Mode: mode,
IsShared: s.isShared > 0,
}
renderAdminTemplate(w, templateEventRule, data)
}
func (s *httpdServer) renderFolderPage(w http.ResponseWriter, r *http.Request, folder vfs.BaseVirtualFolder, func (s *httpdServer) renderFolderPage(w http.ResponseWriter, r *http.Request, folder vfs.BaseVirtualFolder,
mode folderPageMode, error string, mode folderPageMode, error string,
) { ) {
@ -1630,6 +1792,204 @@ func getGroupFromPostFields(r *http.Request) (dataprovider.Group, error) {
return group, nil return group, nil
} }
func getKeyValsFromPostFields(r *http.Request, key, val string) []dataprovider.KeyValue {
var res []dataprovider.KeyValue
for k := range r.Form {
if strings.HasPrefix(k, key) {
formKey := r.Form.Get(k)
idx := strings.TrimPrefix(k, key)
formVal := r.Form.Get(fmt.Sprintf("%s%s", val, idx))
if formKey != "" && formVal != "" {
res = append(res, dataprovider.KeyValue{
Key: formKey,
Value: formVal,
})
}
}
}
return res
}
func getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEventActionOptions, error) {
httpTimeout, err := strconv.Atoi(r.Form.Get("http_timeout"))
if err != nil {
return dataprovider.BaseEventActionOptions{}, fmt.Errorf("invalid http timeout: %w", err)
}
cmdTimeout, err := strconv.Atoi(r.Form.Get("cmd_timeout"))
if err != nil {
return dataprovider.BaseEventActionOptions{}, fmt.Errorf("invalid command timeout: %w", err)
}
options := dataprovider.BaseEventActionOptions{
HTTPConfig: dataprovider.EventActionHTTPConfig{
Endpoint: r.Form.Get("http_endpoint"),
Username: r.Form.Get("http_username"),
Password: getSecretFromFormField(r, "http_password"),
Headers: getKeyValsFromPostFields(r, "http_header_key", "http_header_val"),
Timeout: httpTimeout,
SkipTLSVerify: r.Form.Get("http_skip_tls_verify") != "",
Method: r.Form.Get("http_method"),
QueryParameters: getKeyValsFromPostFields(r, "http_query_key", "http_query_val"),
Body: r.Form.Get("http_body"),
},
CmdConfig: dataprovider.EventActionCommandConfig{
Cmd: r.Form.Get("cmd_path"),
Timeout: cmdTimeout,
EnvVars: getKeyValsFromPostFields(r, "cmd_env_key", "cmd_env_val"),
},
EmailConfig: dataprovider.EventActionEmailConfig{
Recipients: strings.Split(strings.ReplaceAll(r.Form.Get("email_recipients"), " ", ""), ","),
Subject: r.Form.Get("email_subject"),
Body: r.Form.Get("email_body"),
},
}
return options, nil
}
func getEventActionFromPostFields(r *http.Request) (dataprovider.BaseEventAction, error) {
err := r.ParseForm()
if err != nil {
return dataprovider.BaseEventAction{}, err
}
actionType, err := strconv.Atoi(r.Form.Get("type"))
if err != nil {
return dataprovider.BaseEventAction{}, fmt.Errorf("invalid action type: %w", err)
}
options, err := getEventActionOptionsFromPostFields(r)
if err != nil {
return dataprovider.BaseEventAction{}, err
}
action := dataprovider.BaseEventAction{
Name: r.Form.Get("name"),
Description: r.Form.Get("description"),
Type: actionType,
Options: options,
}
return action, nil
}
func getEventRuleConditionsFromPostFields(r *http.Request) (dataprovider.EventConditions, error) {
var schedules []dataprovider.Schedule
var names, fsPaths []dataprovider.ConditionPattern
for k := range r.Form {
if strings.HasPrefix(k, "schedule_hour") {
hour := r.Form.Get(k)
if hour != "" {
idx := strings.TrimPrefix(k, "schedule_hour")
dayOfWeek := r.Form.Get(fmt.Sprintf("schedule_day_of_week%s", idx))
dayOfMonth := r.Form.Get(fmt.Sprintf("schedule_day_of_month%s", idx))
month := r.Form.Get(fmt.Sprintf("schedule_month%s", idx))
schedules = append(schedules, dataprovider.Schedule{
Hours: hour,
DayOfWeek: dayOfWeek,
DayOfMonth: dayOfMonth,
Month: month,
})
}
}
if strings.HasPrefix(k, "name_pattern") {
pattern := r.Form.Get(k)
if pattern != "" {
idx := strings.TrimPrefix(k, "name_pattern")
patternType := r.Form.Get(fmt.Sprintf("type_name_pattern%s", idx))
names = append(names, dataprovider.ConditionPattern{
Pattern: pattern,
InverseMatch: patternType == "inverse",
})
}
}
if strings.HasPrefix(k, "fs_path_pattern") {
pattern := r.Form.Get(k)
if pattern != "" {
idx := strings.TrimPrefix(k, "fs_path_pattern")
patternType := r.Form.Get(fmt.Sprintf("type_fs_path_pattern%s", idx))
fsPaths = append(fsPaths, dataprovider.ConditionPattern{
Pattern: pattern,
InverseMatch: patternType == "inverse",
})
}
}
}
minFileSize, err := strconv.ParseInt(r.Form.Get("fs_min_size"), 10, 64)
if err != nil {
return dataprovider.EventConditions{}, fmt.Errorf("invalid min file size: %w", err)
}
maxFileSize, err := strconv.ParseInt(r.Form.Get("fs_max_size"), 10, 64)
if err != nil {
return dataprovider.EventConditions{}, fmt.Errorf("invalid max file size: %w", err)
}
conditions := dataprovider.EventConditions{
FsEvents: r.Form["fs_events"],
ProviderEvents: r.Form["provider_events"],
Schedules: schedules,
Options: dataprovider.ConditionOptions{
Names: names,
FsPaths: fsPaths,
Protocols: r.Form["fs_protocols"],
ProviderObjects: r.Form["provider_objects"],
MinFileSize: minFileSize,
MaxFileSize: maxFileSize,
ConcurrentExecution: r.Form.Get("concurrent_execution") != "",
},
}
return conditions, nil
}
func getEventRuleActionsFromPostFields(r *http.Request) ([]dataprovider.EventAction, error) {
var actions []dataprovider.EventAction
for k := range r.Form {
if strings.HasPrefix(k, "action_name") {
name := r.Form.Get(k)
if name != "" {
idx := strings.TrimPrefix(k, "action_name")
order, err := strconv.Atoi(r.Form.Get(fmt.Sprintf("action_order%s", idx)))
if err != nil {
return actions, fmt.Errorf("invalid order: %w", err)
}
options := r.Form[fmt.Sprintf("action_options%s", idx)]
actions = append(actions, dataprovider.EventAction{
BaseEventAction: dataprovider.BaseEventAction{
Name: name,
},
Order: order + 1,
Options: dataprovider.EventActionOptions{
IsFailureAction: util.Contains(options, "1"),
StopOnFailure: util.Contains(options, "2"),
ExecuteSync: util.Contains(options, "3"),
},
})
}
}
}
return actions, nil
}
func getEventRuleFromPostFields(r *http.Request) (dataprovider.EventRule, error) {
err := r.ParseForm()
if err != nil {
return dataprovider.EventRule{}, err
}
trigger, err := strconv.Atoi(r.Form.Get("trigger"))
if err != nil {
return dataprovider.EventRule{}, fmt.Errorf("invalid trigger: %w", err)
}
conditions, err := getEventRuleConditionsFromPostFields(r)
if err != nil {
return dataprovider.EventRule{}, err
}
actions, err := getEventRuleActionsFromPostFields(r)
if err != nil {
return dataprovider.EventRule{}, err
}
rule := dataprovider.EventRule{
Name: r.Form.Get("name"),
Description: r.Form.Get("description"),
Trigger: trigger,
Conditions: conditions,
Actions: actions,
}
return rule, nil
}
func (s *httpdServer) handleWebAdminForgotPwd(w http.ResponseWriter, r *http.Request) { func (s *httpdServer) handleWebAdminForgotPwd(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
if !smtp.IsEnabled() { if !smtp.IsEnabled() {
@ -2450,7 +2810,7 @@ func (s *httpdServer) handleWebGetGroups(w http.ResponseWriter, r *http.Request)
func (s *httpdServer) handleWebAddGroupGet(w http.ResponseWriter, r *http.Request) { func (s *httpdServer) handleWebAddGroupGet(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
s.renderGroupPage(w, r, dataprovider.Group{}, groupPageModeAdd, "") s.renderGroupPage(w, r, dataprovider.Group{}, genericPageModeAdd, "")
} }
func (s *httpdServer) handleWebAddGroupPost(w http.ResponseWriter, r *http.Request) { func (s *httpdServer) handleWebAddGroupPost(w http.ResponseWriter, r *http.Request) {
@ -2462,7 +2822,7 @@ func (s *httpdServer) handleWebAddGroupPost(w http.ResponseWriter, r *http.Reque
} }
group, err := getGroupFromPostFields(r) group, err := getGroupFromPostFields(r)
if err != nil { if err != nil {
s.renderGroupPage(w, r, group, groupPageModeAdd, err.Error()) s.renderGroupPage(w, r, group, genericPageModeAdd, err.Error())
return return
} }
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
@ -2472,7 +2832,7 @@ func (s *httpdServer) handleWebAddGroupPost(w http.ResponseWriter, r *http.Reque
} }
err = dataprovider.AddGroup(&group, claims.Username, ipAddr) err = dataprovider.AddGroup(&group, claims.Username, ipAddr)
if err != nil { if err != nil {
s.renderGroupPage(w, r, group, groupPageModeAdd, err.Error()) s.renderGroupPage(w, r, group, genericPageModeAdd, err.Error())
return return
} }
http.Redirect(w, r, webGroupsPath, http.StatusSeeOther) http.Redirect(w, r, webGroupsPath, http.StatusSeeOther)
@ -2483,7 +2843,7 @@ func (s *httpdServer) handleWebUpdateGroupGet(w http.ResponseWriter, r *http.Req
name := getURLParam(r, "name") name := getURLParam(r, "name")
group, err := dataprovider.GroupExists(name) group, err := dataprovider.GroupExists(name)
if err == nil { if err == nil {
s.renderGroupPage(w, r, group, groupPageModeUpdate, "") s.renderGroupPage(w, r, group, genericPageModeUpdate, "")
} else if _, ok := err.(*util.RecordNotFoundError); ok { } else if _, ok := err.(*util.RecordNotFoundError); ok {
s.renderNotFoundPage(w, r, err) s.renderNotFoundPage(w, r, err)
} else { } else {
@ -2509,7 +2869,7 @@ func (s *httpdServer) handleWebUpdateGroupPost(w http.ResponseWriter, r *http.Re
} }
updatedGroup, err := getGroupFromPostFields(r) updatedGroup, err := getGroupFromPostFields(r)
if err != nil { if err != nil {
s.renderGroupPage(w, r, group, groupPageModeUpdate, err.Error()) s.renderGroupPage(w, r, group, genericPageModeUpdate, err.Error())
return return
} }
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
@ -2530,8 +2890,245 @@ func (s *httpdServer) handleWebUpdateGroupPost(w http.ResponseWriter, r *http.Re
err = dataprovider.UpdateGroup(&updatedGroup, group.Users, claims.Username, ipAddr) err = dataprovider.UpdateGroup(&updatedGroup, group.Users, claims.Username, ipAddr)
if err != nil { if err != nil {
s.renderGroupPage(w, r, updatedGroup, groupPageModeUpdate, err.Error()) s.renderGroupPage(w, r, updatedGroup, genericPageModeUpdate, err.Error())
return return
} }
http.Redirect(w, r, webGroupsPath, http.StatusSeeOther) http.Redirect(w, r, webGroupsPath, http.StatusSeeOther)
} }
func (s *httpdServer) getWebEventActions(w http.ResponseWriter, r *http.Request, limit int, minimal bool,
) ([]dataprovider.BaseEventAction, error) {
actions := make([]dataprovider.BaseEventAction, 0, limit)
for {
res, err := dataprovider.GetEventActions(limit, len(actions), dataprovider.OrderASC, minimal)
if err != nil {
s.renderInternalServerErrorPage(w, r, err)
return actions, err
}
actions = append(actions, res...)
if len(res) < limit {
break
}
}
return actions, nil
}
func (s *httpdServer) handleWebGetEventActions(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
limit := defaultQueryLimit
if _, ok := r.URL.Query()["qlimit"]; ok {
var err error
limit, err = strconv.Atoi(r.URL.Query().Get("qlimit"))
if err != nil {
limit = defaultQueryLimit
}
}
actions, err := s.getWebEventActions(w, r, limit, false)
if err != nil {
return
}
data := eventActionsPage{
basePage: s.getBasePageData(pageEventActionsTitle, webAdminEventActionsPath, r),
Actions: actions,
}
renderAdminTemplate(w, templateEventActions, data)
}
func (s *httpdServer) handleWebAddEventActionGet(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
action := dataprovider.BaseEventAction{
Type: dataprovider.ActionTypeHTTP,
}
s.renderEventActionPage(w, r, action, genericPageModeAdd, "")
}
func (s *httpdServer) handleWebAddEventActionPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
s.renderBadRequestPage(w, r, errors.New("invalid token claims"))
return
}
action, err := getEventActionFromPostFields(r)
if err != nil {
s.renderEventActionPage(w, r, action, genericPageModeAdd, err.Error())
return
}
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
s.renderForbiddenPage(w, r, err.Error())
return
}
if err = dataprovider.AddEventAction(&action, claims.Username, ipAddr); err != nil {
s.renderEventActionPage(w, r, action, genericPageModeAdd, err.Error())
return
}
http.Redirect(w, r, webAdminEventActionsPath, http.StatusSeeOther)
}
func (s *httpdServer) handleWebUpdateEventActionGet(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
name := getURLParam(r, "name")
action, err := dataprovider.EventActionExists(name)
if err == nil {
s.renderEventActionPage(w, r, action, genericPageModeUpdate, "")
} else if _, ok := err.(*util.RecordNotFoundError); ok {
s.renderNotFoundPage(w, r, err)
} else {
s.renderInternalServerErrorPage(w, r, err)
}
}
func (s *httpdServer) handleWebUpdateEventActionPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
s.renderBadRequestPage(w, r, errors.New("invalid token claims"))
return
}
name := getURLParam(r, "name")
action, err := dataprovider.EventActionExists(name)
if _, ok := err.(*util.RecordNotFoundError); ok {
s.renderNotFoundPage(w, r, err)
return
} else if err != nil {
s.renderInternalServerErrorPage(w, r, err)
return
}
updatedAction, err := getEventActionFromPostFields(r)
if err != nil {
s.renderEventActionPage(w, r, updatedAction, genericPageModeUpdate, err.Error())
return
}
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
s.renderForbiddenPage(w, r, err.Error())
return
}
updatedAction.ID = action.ID
updatedAction.Name = action.Name
updatedAction.Options.SetEmptySecretsIfNil()
switch updatedAction.Type {
case dataprovider.ActionTypeHTTP:
if updatedAction.Options.HTTPConfig.Password.IsNotPlainAndNotEmpty() {
updatedAction.Options.HTTPConfig.Password = action.Options.HTTPConfig.Password
}
}
err = dataprovider.UpdateEventAction(&updatedAction, claims.Username, ipAddr)
if err != nil {
s.renderEventActionPage(w, r, updatedAction, genericPageModeUpdate, err.Error())
return
}
http.Redirect(w, r, webAdminEventActionsPath, http.StatusSeeOther)
}
func (s *httpdServer) handleWebGetEventRules(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
limit := defaultQueryLimit
if _, ok := r.URL.Query()["qlimit"]; ok {
if lim, err := strconv.Atoi(r.URL.Query().Get("qlimit")); err == nil {
limit = lim
}
}
rules := make([]dataprovider.EventRule, 0, limit)
for {
res, err := dataprovider.GetEventRules(limit, len(rules), dataprovider.OrderASC)
if err != nil {
s.renderInternalServerErrorPage(w, r, err)
return
}
rules = append(rules, res...)
if len(res) < limit {
break
}
}
data := eventRulesPage{
basePage: s.getBasePageData(pageEventRulesTitle, webAdminEventRulesPath, r),
Rules: rules,
}
renderAdminTemplate(w, templateEventRules, data)
}
func (s *httpdServer) handleWebAddEventRuleGet(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
rule := dataprovider.EventRule{
Trigger: dataprovider.EventTriggerFsEvent,
}
s.renderEventRulePage(w, r, rule, genericPageModeAdd, "")
}
func (s *httpdServer) handleWebAddEventRulePost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
s.renderBadRequestPage(w, r, errors.New("invalid token claims"))
return
}
rule, err := getEventRuleFromPostFields(r)
if err != nil {
s.renderEventRulePage(w, r, rule, genericPageModeAdd, err.Error())
return
}
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
err = verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr)
if err != nil {
s.renderForbiddenPage(w, r, err.Error())
return
}
if err = dataprovider.AddEventRule(&rule, claims.Username, ipAddr); err != nil {
s.renderEventRulePage(w, r, rule, genericPageModeAdd, err.Error())
return
}
http.Redirect(w, r, webAdminEventRulesPath, http.StatusSeeOther)
}
func (s *httpdServer) handleWebUpdateEventRuleGet(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
name := getURLParam(r, "name")
rule, err := dataprovider.EventRuleExists(name)
if err == nil {
s.renderEventRulePage(w, r, rule, genericPageModeUpdate, "")
} else if _, ok := err.(*util.RecordNotFoundError); ok {
s.renderNotFoundPage(w, r, err)
} else {
s.renderInternalServerErrorPage(w, r, err)
}
}
func (s *httpdServer) handleWebUpdateEventRulePost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
s.renderBadRequestPage(w, r, errors.New("invalid token claims"))
return
}
name := getURLParam(r, "name")
rule, err := dataprovider.EventRuleExists(name)
if _, ok := err.(*util.RecordNotFoundError); ok {
s.renderNotFoundPage(w, r, err)
return
} else if err != nil {
s.renderInternalServerErrorPage(w, r, err)
return
}
updatedRule, err := getEventRuleFromPostFields(r)
if err != nil {
s.renderEventRulePage(w, r, updatedRule, genericPageModeUpdate, err.Error())
return
}
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
s.renderForbiddenPage(w, r, err.Error())
return
}
updatedRule.ID = rule.ID
updatedRule.Name = rule.Name
err = dataprovider.UpdateEventRule(&updatedRule, claims.Username, ipAddr)
if err != nil {
s.renderEventRulePage(w, r, updatedRule, genericPageModeUpdate, err.Error())
return
}
http.Redirect(w, r, webAdminEventRulesPath, http.StatusSeeOther)
}

View file

@ -46,6 +46,8 @@ const (
apiKeysPath = "/api/v2/apikeys" apiKeysPath = "/api/v2/apikeys"
retentionBasePath = "/api/v2/retention/users" retentionBasePath = "/api/v2/retention/users"
retentionChecksPath = "/api/v2/retention/users/checks" retentionChecksPath = "/api/v2/retention/users/checks"
eventActionsPath = "/api/v2/eventactions"
eventRulesPath = "/api/v2/eventrules"
) )
const ( const (
@ -598,9 +600,227 @@ func GetAPIKeyByID(keyID string, expectedStatusCode int) (dataprovider.APIKey, [
return apiKey, body, err return apiKey, body, err
} }
// AddEventAction adds a new event action
func AddEventAction(action dataprovider.BaseEventAction, expectedStatusCode int) (dataprovider.BaseEventAction, []byte, error) {
var newAction dataprovider.BaseEventAction
var body []byte
asJSON, _ := json.Marshal(action)
resp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(eventActionsPath), bytes.NewBuffer(asJSON),
"application/json", getDefaultToken())
if err != nil {
return newAction, body, err
}
defer resp.Body.Close()
err = checkResponse(resp.StatusCode, expectedStatusCode)
if expectedStatusCode != http.StatusCreated {
body, _ = getResponseBody(resp)
return newAction, body, err
}
if err == nil {
err = render.DecodeJSON(resp.Body, &newAction)
} else {
body, _ = getResponseBody(resp)
}
if err == nil {
err = checkEventAction(action, newAction)
}
return newAction, body, err
}
// UpdateEventAction updates an existing event action
func UpdateEventAction(action dataprovider.BaseEventAction, expectedStatusCode int) (dataprovider.BaseEventAction, []byte, error) {
var newAction dataprovider.BaseEventAction
var body []byte
asJSON, _ := json.Marshal(action)
resp, err := sendHTTPRequest(http.MethodPut, buildURLRelativeToBase(eventActionsPath, url.PathEscape(action.Name)),
bytes.NewBuffer(asJSON), "application/json", getDefaultToken())
if err != nil {
return newAction, body, err
}
defer resp.Body.Close()
body, _ = getResponseBody(resp)
err = checkResponse(resp.StatusCode, expectedStatusCode)
if expectedStatusCode != http.StatusOK {
return newAction, body, err
}
if err == nil {
newAction, body, err = GetEventActionByName(action.Name, expectedStatusCode)
}
if err == nil {
err = checkEventAction(action, newAction)
}
return newAction, body, err
}
// RemoveEventAction removes an existing action and checks the received HTTP Status code against expectedStatusCode.
func RemoveEventAction(action dataprovider.BaseEventAction, expectedStatusCode int) ([]byte, error) {
var body []byte
resp, err := sendHTTPRequest(http.MethodDelete, buildURLRelativeToBase(eventActionsPath, url.PathEscape(action.Name)),
nil, "", getDefaultToken())
if err != nil {
return body, err
}
defer resp.Body.Close()
body, _ = getResponseBody(resp)
return body, checkResponse(resp.StatusCode, expectedStatusCode)
}
// GetEventActionByName gets an event action by name and checks the received HTTP Status code against expectedStatusCode.
func GetEventActionByName(name string, expectedStatusCode int) (dataprovider.BaseEventAction, []byte, error) {
var action dataprovider.BaseEventAction
var body []byte
resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(eventActionsPath, url.PathEscape(name)),
nil, "", getDefaultToken())
if err != nil {
return action, body, err
}
defer resp.Body.Close()
err = checkResponse(resp.StatusCode, expectedStatusCode)
if err == nil && expectedStatusCode == http.StatusOK {
err = render.DecodeJSON(resp.Body, &action)
} else {
body, _ = getResponseBody(resp)
}
return action, body, err
}
// GetEventActions returns a list of event actions and checks the received HTTP Status code against expectedStatusCode.
// The number of results can be limited specifying a limit.
// Some results can be skipped specifying an offset.
func GetEventActions(limit, offset int64, expectedStatusCode int) ([]dataprovider.BaseEventAction, []byte, error) {
var actions []dataprovider.BaseEventAction
var body []byte
url, err := addLimitAndOffsetQueryParams(buildURLRelativeToBase(eventActionsPath), limit, offset)
if err != nil {
return actions, body, err
}
resp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, "", getDefaultToken())
if err != nil {
return actions, body, err
}
defer resp.Body.Close()
err = checkResponse(resp.StatusCode, expectedStatusCode)
if err == nil && expectedStatusCode == http.StatusOK {
err = render.DecodeJSON(resp.Body, &actions)
} else {
body, _ = getResponseBody(resp)
}
return actions, body, err
}
// AddEventRule adds a new event rule
func AddEventRule(rule dataprovider.EventRule, expectedStatusCode int) (dataprovider.EventRule, []byte, error) {
var newRule dataprovider.EventRule
var body []byte
asJSON, _ := json.Marshal(rule)
resp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(eventRulesPath), bytes.NewBuffer(asJSON),
"application/json", getDefaultToken())
if err != nil {
return newRule, body, err
}
defer resp.Body.Close()
err = checkResponse(resp.StatusCode, expectedStatusCode)
if expectedStatusCode != http.StatusCreated {
body, _ = getResponseBody(resp)
return newRule, body, err
}
if err == nil {
err = render.DecodeJSON(resp.Body, &newRule)
} else {
body, _ = getResponseBody(resp)
}
if err == nil {
err = checkEventRule(rule, newRule)
}
return newRule, body, err
}
// UpdateEventRule updates an existing event rule
func UpdateEventRule(rule dataprovider.EventRule, expectedStatusCode int) (dataprovider.EventRule, []byte, error) {
var newRule dataprovider.EventRule
var body []byte
asJSON, _ := json.Marshal(rule)
resp, err := sendHTTPRequest(http.MethodPut, buildURLRelativeToBase(eventRulesPath, url.PathEscape(rule.Name)),
bytes.NewBuffer(asJSON), "application/json", getDefaultToken())
if err != nil {
return newRule, body, err
}
defer resp.Body.Close()
body, _ = getResponseBody(resp)
err = checkResponse(resp.StatusCode, expectedStatusCode)
if expectedStatusCode != http.StatusOK {
return newRule, body, err
}
if err == nil {
newRule, body, err = GetEventRuleByName(rule.Name, expectedStatusCode)
}
if err == nil {
err = checkEventRule(rule, newRule)
}
return newRule, body, err
}
// RemoveEventRule removes an existing rule and checks the received HTTP Status code against expectedStatusCode.
func RemoveEventRule(rule dataprovider.EventRule, expectedStatusCode int) ([]byte, error) {
var body []byte
resp, err := sendHTTPRequest(http.MethodDelete, buildURLRelativeToBase(eventRulesPath, url.PathEscape(rule.Name)),
nil, "", getDefaultToken())
if err != nil {
return body, err
}
defer resp.Body.Close()
body, _ = getResponseBody(resp)
return body, checkResponse(resp.StatusCode, expectedStatusCode)
}
// GetEventRuleByName gets an event rule by name and checks the received HTTP Status code against expectedStatusCode.
func GetEventRuleByName(name string, expectedStatusCode int) (dataprovider.EventRule, []byte, error) {
var rule dataprovider.EventRule
var body []byte
resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(eventRulesPath, url.PathEscape(name)),
nil, "", getDefaultToken())
if err != nil {
return rule, body, err
}
defer resp.Body.Close()
err = checkResponse(resp.StatusCode, expectedStatusCode)
if err == nil && expectedStatusCode == http.StatusOK {
err = render.DecodeJSON(resp.Body, &rule)
} else {
body, _ = getResponseBody(resp)
}
return rule, body, err
}
// GetEventRules returns a list of event rules and checks the received HTTP Status code against expectedStatusCode.
// The number of results can be limited specifying a limit.
// Some results can be skipped specifying an offset.
func GetEventRules(limit, offset int64, expectedStatusCode int) ([]dataprovider.EventRule, []byte, error) {
var rules []dataprovider.EventRule
var body []byte
url, err := addLimitAndOffsetQueryParams(buildURLRelativeToBase(eventRulesPath), limit, offset)
if err != nil {
return rules, body, err
}
resp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, "", getDefaultToken())
if err != nil {
return rules, body, err
}
defer resp.Body.Close()
err = checkResponse(resp.StatusCode, expectedStatusCode)
if err == nil && expectedStatusCode == http.StatusOK {
err = render.DecodeJSON(resp.Body, &rules)
} else {
body, _ = getResponseBody(resp)
}
return rules, body, err
}
// GetQuotaScans gets active quota scans for users and checks the received HTTP Status code against expectedStatusCode. // GetQuotaScans gets active quota scans for users and checks the received HTTP Status code against expectedStatusCode.
func GetQuotaScans(expectedStatusCode int) ([]common.ActiveQuotaScan, []byte, error) { func GetQuotaScans(expectedStatusCode int) ([]dataprovider.ActiveQuotaScan, []byte, error) {
var quotaScans []common.ActiveQuotaScan var quotaScans []dataprovider.ActiveQuotaScan
var body []byte var body []byte
resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(quotaScanPath), nil, "", getDefaultToken()) resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(quotaScanPath), nil, "", getDefaultToken())
if err != nil { if err != nil {
@ -843,8 +1063,8 @@ func GetFolders(limit int64, offset int64, expectedStatusCode int) ([]vfs.BaseVi
} }
// GetFoldersQuotaScans gets active quota scans for folders and checks the received HTTP Status code against expectedStatusCode. // GetFoldersQuotaScans gets active quota scans for folders and checks the received HTTP Status code against expectedStatusCode.
func GetFoldersQuotaScans(expectedStatusCode int) ([]common.ActiveVirtualFolderQuotaScan, []byte, error) { func GetFoldersQuotaScans(expectedStatusCode int) ([]dataprovider.ActiveVirtualFolderQuotaScan, []byte, error) {
var quotaScans []common.ActiveVirtualFolderQuotaScan var quotaScans []dataprovider.ActiveVirtualFolderQuotaScan
var body []byte var body []byte
resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(quotaScanVFolderPath), nil, "", getDefaultToken()) resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(quotaScanVFolderPath), nil, "", getDefaultToken())
if err != nil { if err != nil {
@ -1087,7 +1307,149 @@ func getResponseBody(resp *http.Response) ([]byte, error) {
return io.ReadAll(resp.Body) return io.ReadAll(resp.Body)
} }
func checkGroup(expected dataprovider.Group, actual dataprovider.Group) error { func checkEventAction(expected, actual dataprovider.BaseEventAction) error {
if expected.ID <= 0 {
if actual.ID <= 0 {
return errors.New("actual action ID must be > 0")
}
} else {
if actual.ID != expected.ID {
return errors.New("action ID mismatch")
}
}
if dataprovider.ConvertName(expected.Name) != actual.Name {
return errors.New("name mismatch")
}
if expected.Description != actual.Description {
return errors.New("description mismatch")
}
if expected.Type != actual.Type {
return errors.New("type mismatch")
}
if err := compareEventActionCmdConfigFields(expected.Options.CmdConfig, actual.Options.CmdConfig); err != nil {
return err
}
if err := compareEventActionEmailConfigFields(expected.Options.EmailConfig, actual.Options.EmailConfig); err != nil {
return err
}
return compareEventActionHTTPConfigFields(expected.Options.HTTPConfig, actual.Options.HTTPConfig)
}
func checkEventSchedules(expected, actual []dataprovider.Schedule) error {
if len(expected) != len(actual) {
return errors.New("schedules mismatch")
}
for _, ex := range expected {
found := false
for _, ac := range actual {
if ac.DayOfMonth == ex.DayOfMonth && ac.DayOfWeek == ex.DayOfWeek && ac.Hours == ex.Hours && ac.Month == ex.Month {
found = true
break
}
}
if !found {
return errors.New("schedules content mismatch")
}
}
return nil
}
func compareConditionPatternOptions(expected, actual []dataprovider.ConditionPattern) error {
if len(expected) != len(actual) {
return errors.New("condition pattern mismatch")
}
for _, ex := range expected {
found := false
for _, ac := range actual {
if ac.Pattern == ex.Pattern && ac.InverseMatch == ex.InverseMatch {
found = true
break
}
}
if !found {
return errors.New("condition pattern content mismatch")
}
}
return nil
}
func checkEventConditionOptions(expected, actual dataprovider.ConditionOptions) error {
if err := compareConditionPatternOptions(expected.Names, actual.Names); err != nil {
return errors.New("condition names mismatch")
}
if err := compareConditionPatternOptions(expected.FsPaths, actual.FsPaths); err != nil {
return errors.New("condition fs_paths mismatch")
}
if len(expected.Protocols) != len(actual.Protocols) {
return errors.New("condition protocols mismatch")
}
for _, v := range expected.Protocols {
if !util.Contains(actual.Protocols, v) {
return errors.New("condition protocols content mismatch")
}
}
if len(expected.ProviderObjects) != len(actual.ProviderObjects) {
return errors.New("condition provider objects mismatch")
}
for _, v := range expected.ProviderObjects {
if !util.Contains(actual.ProviderObjects, v) {
return errors.New("condition provider objects content mismatch")
}
}
if expected.MinFileSize != actual.MinFileSize {
return errors.New("condition min file size mismatch")
}
if expected.MaxFileSize != actual.MaxFileSize {
return errors.New("condition max file size mismatch")
}
return nil
}
func checkEventConditions(expected, actual dataprovider.EventConditions) error {
if len(expected.FsEvents) != len(actual.FsEvents) {
return errors.New("fs events mismatch")
}
for _, v := range expected.FsEvents {
if !util.Contains(actual.FsEvents, v) {
return errors.New("fs events content mismatch")
}
}
if len(expected.ProviderEvents) != len(actual.ProviderEvents) {
return errors.New("provider events mismatch")
}
for _, v := range expected.ProviderEvents {
if !util.Contains(actual.ProviderEvents, v) {
return errors.New("provider events content mismatch")
}
}
if err := checkEventConditionOptions(expected.Options, actual.Options); err != nil {
return err
}
return checkEventSchedules(expected.Schedules, actual.Schedules)
}
func checkEventRuleActions(expected, actual []dataprovider.EventAction) error {
if len(expected) != len(actual) {
return errors.New("actions mismatch")
}
for _, ex := range expected {
found := false
for _, ac := range actual {
if ex.Name == ac.Name && ex.Order == ac.Order && ex.Options.ExecuteSync == ac.Options.ExecuteSync &&
ex.Options.IsFailureAction == ac.Options.IsFailureAction && ex.Options.StopOnFailure == ac.Options.StopOnFailure {
found = true
break
}
}
if !found {
return errors.New("actions contents mismatch")
}
}
return nil
}
func checkEventRule(expected, actual dataprovider.EventRule) error {
if expected.ID <= 0 { if expected.ID <= 0 {
if actual.ID <= 0 { if actual.ID <= 0 {
return errors.New("actual group ID must be > 0") return errors.New("actual group ID must be > 0")
@ -1103,6 +1465,43 @@ func checkGroup(expected dataprovider.Group, actual dataprovider.Group) error {
if expected.Description != actual.Description { if expected.Description != actual.Description {
return errors.New("description mismatch") return errors.New("description mismatch")
} }
if actual.CreatedAt == 0 {
return errors.New("created_at unset")
}
if actual.UpdatedAt == 0 {
return errors.New("updated_at unset")
}
if expected.Trigger != actual.Trigger {
return errors.New("trigger mismatch")
}
if err := checkEventConditions(expected.Conditions, actual.Conditions); err != nil {
return err
}
return checkEventRuleActions(expected.Actions, actual.Actions)
}
func checkGroup(expected, actual dataprovider.Group) error {
if expected.ID <= 0 {
if actual.ID <= 0 {
return errors.New("actual group ID must be > 0")
}
} else {
if actual.ID != expected.ID {
return errors.New("group ID mismatch")
}
}
if dataprovider.ConvertName(expected.Name) != actual.Name {
return errors.New("name mismatch")
}
if expected.Description != actual.Description {
return errors.New("description mismatch")
}
if actual.CreatedAt == 0 {
return errors.New("created_at unset")
}
if actual.UpdatedAt == 0 {
return errors.New("updated_at unset")
}
if err := compareEqualGroupSettingsFields(expected.UserSettings.BaseGroupUserSettings, if err := compareEqualGroupSettingsFields(expected.UserSettings.BaseGroupUserSettings,
actual.UserSettings.BaseGroupUserSettings); err != nil { actual.UserSettings.BaseGroupUserSettings); err != nil {
return err return err
@ -1202,12 +1601,12 @@ func checkAdmin(expected, actual *dataprovider.Admin) error {
return errors.New("permissions content mismatch") return errors.New("permissions content mismatch")
} }
} }
if len(expected.Filters.AllowList) != len(actual.Filters.AllowList) {
return errors.New("allow list mismatch")
}
if expected.Filters.AllowAPIKeyAuth != actual.Filters.AllowAPIKeyAuth { if expected.Filters.AllowAPIKeyAuth != actual.Filters.AllowAPIKeyAuth {
return errors.New("allow_api_key_auth mismatch") return errors.New("allow_api_key_auth mismatch")
} }
if len(expected.Filters.AllowList) != len(actual.Filters.AllowList) {
return errors.New("allow list mismatch")
}
for _, v := range expected.Filters.AllowList { for _, v := range expected.Filters.AllowList {
if !util.Contains(actual.Filters.AllowList, v) { if !util.Contains(actual.Filters.AllowList, v) {
return errors.New("allow list content mismatch") return errors.New("allow list content mismatch")
@ -1748,6 +2147,87 @@ func compareUserFilePatternsFilters(expected sdk.BaseUserFilters, actual sdk.Bas
return nil return nil
} }
func compareKeyValues(expected, actual []dataprovider.KeyValue) error {
if len(expected) != len(actual) {
return errors.New("kay values mismatch")
}
for _, ex := range expected {
found := false
for _, ac := range actual {
if ac.Key == ex.Key && ac.Value == ex.Value {
found = true
break
}
}
if !found {
return errors.New("kay values mismatch")
}
}
return nil
}
func compareEventActionHTTPConfigFields(expected, actual dataprovider.EventActionHTTPConfig) error {
if expected.Endpoint != actual.Endpoint {
return errors.New("http endpoint mismatch")
}
if expected.Username != actual.Username {
return errors.New("http username mismatch")
}
if err := checkEncryptedSecret(expected.Password, actual.Password); err != nil {
return err
}
if err := compareKeyValues(expected.Headers, actual.Headers); err != nil {
return errors.New("http headers mismatch")
}
if expected.Timeout != actual.Timeout {
return errors.New("http timeout mismatch")
}
if expected.SkipTLSVerify != actual.SkipTLSVerify {
return errors.New("http skip TLS verify mismatch")
}
if expected.Method != actual.Method {
return errors.New("http method mismatch")
}
if err := compareKeyValues(expected.QueryParameters, actual.QueryParameters); err != nil {
return errors.New("http query parameters mismatch")
}
if expected.Body != actual.Body {
return errors.New("http body mismatch")
}
return nil
}
func compareEventActionEmailConfigFields(expected, actual dataprovider.EventActionEmailConfig) error {
if len(expected.Recipients) != len(actual.Recipients) {
return errors.New("email recipients mismatch")
}
for _, v := range expected.Recipients {
if !util.Contains(actual.Recipients, v) {
return errors.New("email recipients content mismatch")
}
}
if expected.Subject != actual.Subject {
return errors.New("email subject mismatch")
}
if expected.Body != actual.Body {
return errors.New("email body mismatch")
}
return nil
}
func compareEventActionCmdConfigFields(expected, actual dataprovider.EventActionCommandConfig) error {
if expected.Cmd != actual.Cmd {
return errors.New("command mismatch")
}
if expected.Timeout != actual.Timeout {
return errors.New("cmd timeout mismatch")
}
if err := compareKeyValues(expected.EnvVars, actual.EnvVars); err != nil {
return errors.New("cmd env vars mismatch")
}
return nil
}
func compareEqualGroupSettingsFields(expected sdk.BaseGroupUserSettings, actual sdk.BaseGroupUserSettings) error { func compareEqualGroupSettingsFields(expected sdk.BaseGroupUserSettings, actual sdk.BaseGroupUserSettings) error {
if expected.HomeDir != actual.HomeDir { if expected.HomeDir != actual.HomeDir {
return errors.New("home dir mismatch") return errors.New("home dir mismatch")

View file

@ -16,6 +16,7 @@ tags:
- name: metadata - name: metadata
- name: user APIs - name: user APIs
- name: public shares - name: public shares
- name: event manager
info: info:
title: SFTPGo title: SFTPGo
description: | description: |
@ -1450,7 +1451,7 @@ paths:
schema: schema:
$ref: '#/components/schemas/ApiResponse' $ref: '#/components/schemas/ApiResponse'
example: example:
message: User updated message: Folder updated
'400': '400':
$ref: '#/components/responses/BadRequest' $ref: '#/components/responses/BadRequest'
'401': '401':
@ -1625,7 +1626,7 @@ paths:
schema: schema:
$ref: '#/components/schemas/ApiResponse' $ref: '#/components/schemas/ApiResponse'
example: example:
message: User updated message: Group updated
'400': '400':
$ref: '#/components/responses/BadRequest' $ref: '#/components/responses/BadRequest'
'401': '401':
@ -1641,7 +1642,7 @@ paths:
delete: delete:
tags: tags:
- groups - groups
summary: Delete summary: Delete group
description: Deletes an existing group description: Deletes an existing group
operationId: delete_group operationId: delete_group
responses: responses:
@ -1652,7 +1653,357 @@ paths:
schema: schema:
$ref: '#/components/schemas/ApiResponse' $ref: '#/components/schemas/ApiResponse'
example: example:
message: User deleted message: Group deleted
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/eventactions:
get:
tags:
- event manager
summary: Get event actions
description: Returns an array with one or more event actions
operationId: get_event_actons
parameters:
- in: query
name: offset
schema:
type: integer
minimum: 0
default: 0
required: false
- in: query
name: limit
schema:
type: integer
minimum: 1
maximum: 500
default: 100
required: false
description: 'The maximum number of items to return. Max value is 500, default is 100'
- in: query
name: order
required: false
description: Ordering actions by name. Default ASC
schema:
type: string
enum:
- ASC
- DESC
example: ASC
responses:
'200':
description: successful operation
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/BaseEventAction'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'500':
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
post:
tags:
- event manager
summary: Add event action
operationId: add_event_action
description: Adds a new event actions
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/BaseEventAction'
responses:
'201':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/BaseEventAction'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'500':
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
'/eventactions/{name}':
parameters:
- name: name
in: path
description: action name
required: true
schema:
type: string
get:
tags:
- event manager
summary: Find event actions by name
description: Returns the event action with the given name if it exists.
operationId: get_event_action_by_name
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/BaseEventAction'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
put:
tags:
- event manager
summary: Update event action
description: Updates an existing event action
operationId: update_event_action
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/BaseEventAction'
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
message: Event action updated
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
delete:
tags:
- event manager
summary: Delete event action
description: Deletes an existing event action
operationId: delete_event_action
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
message: Event action deleted
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/eventrules:
get:
tags:
- event manager
summary: Get event rules
description: Returns an array with one or more event rules
operationId: get_event_rules
parameters:
- in: query
name: offset
schema:
type: integer
minimum: 0
default: 0
required: false
- in: query
name: limit
schema:
type: integer
minimum: 1
maximum: 500
default: 100
required: false
description: 'The maximum number of items to return. Max value is 500, default is 100'
- in: query
name: order
required: false
description: Ordering rules by name. Default ASC
schema:
type: string
enum:
- ASC
- DESC
example: ASC
responses:
'200':
description: successful operation
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/EventRule'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'500':
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
post:
tags:
- event manager
summary: Add event rule
operationId: add_event_rule
description: Adds a new event rule
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/EventRuleMinimal'
responses:
'201':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/EventRule'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'500':
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
'/eventrules/{name}':
parameters:
- name: name
in: path
description: rule name
required: true
schema:
type: string
get:
tags:
- event manager
summary: Find event rules by name
description: Returns the event rule with the given name if it exists.
operationId: get_event_rile_by_name
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/EventRule'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
put:
tags:
- event manager
summary: Update event rule
description: Updates an existing event rule
operationId: update_event_rule
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/EventRuleMinimal'
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
message: Event rules updated
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
delete:
tags:
- event manager
summary: Delete event rule
description: Deletes an existing event rule
operationId: delete_event_rule
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
example:
message: Event rules deleted
'400': '400':
$ref: '#/components/responses/BadRequest' $ref: '#/components/responses/BadRequest'
'401': '401':
@ -3942,6 +4293,7 @@ components:
- retention_checks - retention_checks
- metadata_checks - metadata_checks
- view_events - view_events
- manage_event_rules
description: | description: |
Admin permissions: Admin permissions:
* `*` - all permissions are granted * `*` - all permissions are granted
@ -3961,6 +4313,7 @@ components:
* `retention_checks` - view and start retention checks is allowed * `retention_checks` - view and start retention checks is allowed
* `metadata_checks` - view and start metadata checks is allowed * `metadata_checks` - view and start metadata checks is allowed
* `view_events` - view and search filesystem and provider events is allowed * `view_events` - view and search filesystem and provider events is allowed
* `manage_event_rules` - manage event actions and rules is allowed
FsProviders: FsProviders:
type: integer type: integer
enum: enum:
@ -3980,6 +4333,36 @@ components:
* `4` - Local filesystem encrypted * `4` - Local filesystem encrypted
* `5` - SFTP * `5` - SFTP
* `6` - HTTP filesystem * `6` - HTTP filesystem
EventActionTypes:
type: integer
enum:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
description: |
Supported event action types:
* `1` - HTTP
* `2` - Command
* `3` - Email
* `4` - Backup
* `5` - User quota reset
* `6` - Folder quota reset
* `7` - Transfer quota reset
EventTriggerTypes:
type: integer
enum:
- 1
- 2
- 3
description: |
Supported event trigger types:
* `1` - Filesystem event
* `2` - Provider event
* `3` - Schedule
LoginMethods: LoginMethods:
type: string type: string
enum: enum:
@ -5289,20 +5672,6 @@ components:
type: boolean type: boolean
mfa: mfa:
$ref: '#/components/schemas/MFAStatus' $ref: '#/components/schemas/MFAStatus'
BanStatus:
type: object
properties:
date_time:
type: string
format: date-time
nullable: true
description: if null the host is not banned
ScoreStatus:
type: object
properties:
score:
type: integer
description: if 0 the host is not listed
Share: Share:
type: object type: object
properties: properties:
@ -5574,6 +5943,269 @@ components:
type: string type: string
instance_id: instance_id:
type: string type: string
KeyValue:
type: object
properties:
key:
type: string
value:
type: string
EventActionHTTPConfig:
type: object
properties:
endpoint:
type: string
description: HTTP endpoint
example: https://example.com
username:
type: string
password:
$ref: '#/components/schemas/Secret'
headers:
type: array
items:
$ref: '#/components/schemas/KeyValue'
description: headers to add
timeout:
type: integer
minimum: 1
maximum: 120
skip_tls_verify:
type: boolean
description: 'if enabled the HTTP client accepts any TLS certificate presented by the server and any host name in that certificate. In this mode, TLS is susceptible to man-in-the-middle attacks. This should be used only for testing.'
method:
type: string
enum:
- GET
- POST
- PUT
query_parameters:
type: array
items:
$ref: '#/components/schemas/KeyValue'
body:
type: string
description: HTTP POST/PUT body
EventActionCommandConfig:
type: object
properties:
cmd:
type: string
description: absolute path to the command to execute
timeout:
type: integer
minimum: 1
maximum: 120
env_vars:
type: array
items:
$ref: '#/components/schemas/KeyValue'
EventActionEmailConfig:
type: object
properties:
recipients:
type: array
items:
type: string
subject:
type: string
body:
type: string
BaseEventActionOptions:
type: object
properties:
http_config:
$ref: '#/components/schemas/EventActionHTTPConfig'
cmd_config:
$ref: '#/components/schemas/EventActionCommandConfig'
email_config:
$ref: '#/components/schemas/EventActionEmailConfig'
BaseEventAction:
type: object
properties:
id:
type: integer
format: int32
minimum: 1
name:
type: string
description: unique name
description:
type: string
description: optional description
type:
$ref: '#/components/schemas/EventActionTypes'
options:
$ref: '#/components/schemas/BaseEventActionOptions'
rules:
type: array
items:
type: string
description: list of event rules names associated with this action
EventActionOptions:
type: object
properties:
is_failure_action:
type: boolean
stop_on_failure:
type: boolean
execute_sync:
type: boolean
EventAction:
allOf:
- $ref: '#/components/schemas/BaseEventAction'
- type: object
properties:
order:
type: integer
description: execution order
relation_options:
$ref: '#/components/schemas/EventActionOptions'
EventActionMinimal:
type: object
properties:
name:
type: string
order:
type: integer
description: execution order
relation_options:
$ref: '#/components/schemas/EventActionOptions'
ConditionPattern:
type: object
properties:
pattern:
type: string
inverse_match:
type: boolean
ConditionOptions:
type: object
properties:
names:
type: array
items:
$ref: '#/components/schemas/ConditionPattern'
fs_paths:
type: array
items:
$ref: '#/components/schemas/ConditionPattern'
protocols:
type: array
items:
type: string
enum:
- SFTP
- SCP
- SSH
- FTP
- DAV
- HTTP
- HTTPShare
- OIDC
provider_objects:
type: array
items:
type: string
enum:
- user
- group
- admin
- api_key
- share
- event_action
- event_rule
min_size:
type: integer
format: int64
max_size:
type: integer
format: int64
concurrent_execution:
type: boolean
description: allow concurrent execution from multiple nodes
Schedule:
type: object
properties:
hour:
type: string
day_of_week:
type: string
day_of_month:
type: string
month:
type: string
EventConditions:
type: object
properties:
fs_events:
type: array
items:
type: string
enum:
- upload
- download
- delete
- rename
- mkdir
- rmdir
- ssh_cmd
provider_events:
type: array
items:
type: string
enum:
- add
- update
- delete
schedules:
type: array
items:
$ref: '#/components/schemas/Schedule'
options:
$ref: '#/components/schemas/ConditionOptions'
BaseEventRule:
type: object
properties:
id:
type: integer
format: int32
minimum: 1
name:
type: string
description: unique name
description:
type: string
description: optional description
created_at:
type: integer
format: int64
description: creation time as unix timestamp in milliseconds
updated_at:
type: integer
format: int64
description: last update time as unix timestamp in millisecond
trigger:
$ref: '#/components/schemas/EventTriggerTypes'
conditions:
$ref: '#/components/schemas/EventConditions'
EventRule:
allOf:
- $ref: '#/components/schemas/BaseEventRule'
- type: object
properties:
actions:
type: array
items:
$ref: '#/components/schemas/EventAction'
EventRuleMinimal:
allOf:
- $ref: '#/components/schemas/BaseEventRule'
- type: object
properties:
actions:
type: array
items:
$ref: '#/components/schemas/EventActionMinimal'
ApiResponse: ApiResponse:
type: object type: object
properties: properties:

View file

@ -351,5 +351,13 @@ func (s *Service) restoreDump(dump *dataprovider.BackupData) error {
if err != nil { if err != nil {
return fmt.Errorf("unable to restore API keys from file %#v: %v", s.LoadDataFrom, err) return fmt.Errorf("unable to restore API keys from file %#v: %v", s.LoadDataFrom, err)
} }
err = httpd.RestoreEventActions(dump.EventActions, s.LoadDataFrom, s.LoadDataMode, dataprovider.ActionExecutorSystem, "")
if err != nil {
return fmt.Errorf("unable to restore event actions from file %#v: %v", s.LoadDataFrom, err)
}
err = httpd.RestoreEventRules(dump.EventRules, s.LoadDataFrom, s.LoadDataMode, dataprovider.ActionExecutorSystem, "")
if err != nil {
return fmt.Errorf("unable to restore event rules from file %#v: %v", s.LoadDataFrom, err)
}
return nil return nil
} }

View file

@ -135,7 +135,7 @@ func newMockOsFs(err, statErr error, atomicUpload bool, connectionID, rootDir st
} }
func TestRemoveNonexistentQuotaScan(t *testing.T) { func TestRemoveNonexistentQuotaScan(t *testing.T) {
assert.False(t, common.QuotaScans.RemoveUserQuotaScan("username")) assert.False(t, dataprovider.QuotaScans.RemoveUserQuotaScan("username"))
} }
func TestGetOSOpenFlags(t *testing.T) { func TestGetOSOpenFlags(t *testing.T) {

View file

@ -4403,14 +4403,14 @@ func TestQuotaScan(t *testing.T) {
} }
func TestMultipleQuotaScans(t *testing.T) { func TestMultipleQuotaScans(t *testing.T) {
res := common.QuotaScans.AddUserQuotaScan(defaultUsername) res := dataprovider.QuotaScans.AddUserQuotaScan(defaultUsername)
assert.True(t, res) assert.True(t, res)
res = common.QuotaScans.AddUserQuotaScan(defaultUsername) res = dataprovider.QuotaScans.AddUserQuotaScan(defaultUsername)
assert.False(t, res, "add quota must fail if another scan is already active") assert.False(t, res, "add quota must fail if another scan is already active")
assert.True(t, common.QuotaScans.RemoveUserQuotaScan(defaultUsername)) assert.True(t, dataprovider.QuotaScans.RemoveUserQuotaScan(defaultUsername))
activeScans := common.QuotaScans.GetUsersQuotaScans() activeScans := dataprovider.QuotaScans.GetUsersQuotaScans()
assert.Equal(t, 0, len(activeScans)) assert.Equal(t, 0, len(activeScans))
assert.False(t, common.QuotaScans.RemoveUserQuotaScan(defaultUsername)) assert.False(t, dataprovider.QuotaScans.RemoveUserQuotaScan(defaultUsername))
} }
func TestQuotaLimits(t *testing.T) { func TestQuotaLimits(t *testing.T) {
@ -6762,15 +6762,15 @@ func TestVirtualFolderQuotaScan(t *testing.T) {
func TestVFolderMultipleQuotaScan(t *testing.T) { func TestVFolderMultipleQuotaScan(t *testing.T) {
folderName := "folder_name" folderName := "folder_name"
res := common.QuotaScans.AddVFolderQuotaScan(folderName) res := dataprovider.QuotaScans.AddVFolderQuotaScan(folderName)
assert.True(t, res) assert.True(t, res)
res = common.QuotaScans.AddVFolderQuotaScan(folderName) res = dataprovider.QuotaScans.AddVFolderQuotaScan(folderName)
assert.False(t, res) assert.False(t, res)
res = common.QuotaScans.RemoveVFolderQuotaScan(folderName) res = dataprovider.QuotaScans.RemoveVFolderQuotaScan(folderName)
assert.True(t, res) assert.True(t, res)
activeScans := common.QuotaScans.GetVFoldersQuotaScans() activeScans := dataprovider.QuotaScans.GetVFoldersQuotaScans()
assert.Len(t, activeScans, 0) assert.Len(t, activeScans, 0)
res = common.QuotaScans.RemoveVFolderQuotaScan(folderName) res = dataprovider.QuotaScans.RemoveVFolderQuotaScan(folderName)
assert.False(t, res) assert.False(t, res)
} }

View file

@ -231,12 +231,7 @@
"create_default_admin": false, "create_default_admin": false,
"naming_rules": 1, "naming_rules": 1,
"is_shared": 0, "is_shared": 0,
"backups_path": "backups", "backups_path": "backups"
"auto_backup": {
"enabled": true,
"hour": "0",
"day_of_week": "*"
}
}, },
"httpd": { "httpd": {
"bindings": [ "bindings": [

View file

@ -22,7 +22,7 @@ const (
// EmailContentType defines the support content types for email body // EmailContentType defines the support content types for email body
type EmailContentType int type EmailContentType int
// Supporte email body content type // Supported email body content type
const ( const (
EmailContentTypeTextPlain EmailContentType = iota EmailContentTypeTextPlain EmailContentType = iota
EmailContentTypeTextHTML EmailContentTypeTextHTML
@ -169,7 +169,7 @@ func RenderPasswordResetTemplate(buf *bytes.Buffer, data any) error {
} }
// SendEmail tries to send an email using the specified parameters. // SendEmail tries to send an email using the specified parameters.
func SendEmail(to, subject, body string, contentType EmailContentType) error { func SendEmail(to []string, subject, body string, contentType EmailContentType) error {
if smtpServer == nil { if smtpServer == nil {
return errors.New("smtp: not configured") return errors.New("smtp: not configured")
} }
@ -184,7 +184,7 @@ func SendEmail(to, subject, body string, contentType EmailContentType) error {
} else { } else {
email.SetFrom(smtpServer.Username) email.SetFrom(smtpServer.Username)
} }
email.AddTo(to).SetSubject(subject) email.AddTo(to...).SetSubject(subject)
switch contentType { switch contentType {
case EmailContentTypeTextPlain: case EmailContentTypeTextPlain:
email.SetBody(mail.TextPlain, body) email.SetBody(mail.TextPlain, body)

File diff suppressed because one or more lines are too long

View file

@ -74,6 +74,22 @@
</li> </li>
{{end}} {{end}}
{{ if .LoggedAdmin.HasPermission "manage_event_rules"}}
<li class="nav-item {{if .IsEventManagerPage}}active{{end}}">
<a class="nav-link {{if not .IsEventManagerPage}}collapsed{{end}}" href="#" data-toggle="collapse" data-target="#collapseEventManager"
aria-expanded="true" aria-controls="collapseEventManager">
<i class="fas fa-calendar-alt"></i>
<span>Event Manager</span>
</a>
<div id="collapseEventManager" class="collapse {{if .IsEventManagerPage}}show{{end}}" aria-labelledby="headingEventManager" data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<a class="collapse-item {{if eq .CurrentURL .EventRulesURL}}active{{end}}" href="{{.EventRulesURL}}">{{.EventRulesTitle}}</a>
<a class="collapse-item {{if eq .CurrentURL .EventActionsURL}}active{{end}}" href="{{.EventActionsURL}}">{{.EventActionsTitle}}</a>
</div>
</div>
</li>
{{end}}
{{ if .LoggedAdmin.HasPermission "view_conns"}} {{ if .LoggedAdmin.HasPermission "view_conns"}}
<li class="nav-item {{if eq .CurrentURL .ConnectionsURL}}active{{end}}"> <li class="nav-item {{if eq .CurrentURL .ConnectionsURL}}active{{end}}">
<a class="nav-link" href="{{.ConnectionsURL}}"> <a class="nav-link" href="{{.ConnectionsURL}}">
@ -142,6 +158,7 @@
<!-- Topbar Navbar --> <!-- Topbar Navbar -->
<ul class="navbar-nav ml-auto"> <ul class="navbar-nav ml-auto">
{{block "additionalnavitems" .}}{{end}}
<!-- Nav Item - User Information --> <!-- Nav Item - User Information -->
<li class="nav-item dropdown no-arrow"> <li class="nav-item dropdown no-arrow">

View file

@ -0,0 +1,511 @@
{{template "base" .}}
{{define "title"}}{{.Title}}{{end}}
{{define "extra_css"}}
<link href="{{.StaticURL}}/vendor/bootstrap-select/css/bootstrap-select.min.css" rel="stylesheet">
{{end}}
{{define "additionalnavitems"}}
<li class="nav-item dropdown no-arrow mx-1">
<a class="nav-link dropdown-toggle" href="#" id="editorDropdown" role="button"
data-toggle="modal" data-target="#infoModal">
<i class="fas fa-info fa-fw"></i>
</a>
</li>
<div class="topbar-divider d-none d-sm-block"></div>
{{end}}
{{define "page_body"}}
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">{{.Title}}</h6>
</div>
<div class="card-body">
{{if .Error}}
<div class="card mb-4 border-left-warning">
<div class="card-body text-form-error">{{.Error}}</div>
</div>
{{end}}
<form id="eventaction_form" action="{{.CurrentURL}}" method="POST" autocomplete="off">
<div class="form-group row">
<label for="idName" class="col-sm-2 col-form-label">Name</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idName" name="name" placeholder=""
value="{{.Action.Name}}" maxlength="255" autocomplete="nope" required {{if eq .Mode 2}}readonly{{end}}>
</div>
</div>
<div class="form-group row">
<label for="idDescription" class="col-sm-2 col-form-label">Description</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idDescription" name="description" placeholder=""
value="{{.Action.Description}}" maxlength="255" aria-describedby="descriptionHelpBlock">
<small id="descriptionHelpBlock" class="form-text text-muted">
Optional description
</small>
</div>
</div>
<div class="form-group row">
<label for="idType" class="col-sm-2 col-form-label">Type</label>
<div class="col-sm-10">
<select class="form-control selectpicker" id="idType" name="type" onchange="onTypeChanged(this.value)">
{{- range .ActionTypes}}
<option value="{{.Value}}" {{if eq $.Action.Type .Value }}selected{{end}}>{{.Name}}</option>
{{- end}}
</select>
</div>
</div>
<div class="form-group row action-type action-http">
<label for="idHTTPEndpoint" class="col-sm-2 col-form-label">Endpoint</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idHTTPEndpoint" name="http_endpoint" placeholder=""
aria-describedby="HTTPEndpointHelpBlock" value="{{.Action.Options.HTTPConfig.Endpoint}}">
<small id="HTTPEndpointHelpBlock" class="form-text text-muted">
Endpoint URL, i.e https://host:port/path
</small>
</div>
</div>
<div class="form-group row action-type action-http">
<label for="idHTTPUsername" class="col-sm-2 col-form-label">Username</label>
<div class="col-sm-3">
<input type="text" class="form-control" id="idHTTPUsername" name="http_username" placeholder=""
aria-describedby="usernameHelpBlock" value="{{.Action.Options.HTTPConfig.Username}}" maxlength="255">
<small id="httpBodyHelpBlock" class="form-text text-muted">
Placeholders are supported
</small>
</div>
<div class="col-sm-2"></div>
<label for="idHTTPPassword" class="col-sm-2 col-form-label">Password</label>
<div class="col-sm-3">
<input type="password" class="form-control" id="idHTTPPassword" name="http_password" placeholder=""
value="{{if .Action.Options.HTTPConfig.Password.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.Action.Options.HTTPConfig.Password.GetPayload}}{{end}}">
</div>
</div>
<div class="card bg-light mb-3 action-type action-http">
<div class="card-header">
<b>HTTP headers</b>
</div>
<div class="card-body">
<h6 class="card-title mb-4">Placeholders are supported in header values.</h6>
<div class="form-group row">
<div class="col-md-12 form_field_http_headers_outer">
{{range $idx, $val := .Action.Options.HTTPConfig.Headers}}
<div class="row form_field_http_headers_outer_row">
<div class="form-group col-md-5">
<input type="text" class="form-control" id="idHTTPHeaderKey{{$idx}}" name="http_header_key{{$idx}}" placeholder="Enter key" value="{{$val.Key}}">
</div>
<div class="form-group col-md-5">
<input type="text" class="form-control" id="idHTTPHeaderVal{{$idx}}" name="http_header_val{{$idx}}" placeholder="Enter value" value="{{$val.Value}}">
</div>
<div class="form-group col-md-1"></div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_http_header_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{else}}
<div class="row form_field_http_headers_outer_row">
<div class="form-group col-md-5">
<input type="text" class="form-control" id="idHTTPHeaderKey0" name="http_header_key0" placeholder="Enter key" value="">
</div>
<div class="form-group col-md-5">
<input type="text" class="form-control" id="idHTTPHeaderVal0" name="http_header_val0" placeholder="Enter value" value="">
</div>
<div class="form-group col-md-1"></div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_http_header_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{end}}
</div>
</div>
<div class="row mx-1">
<button type="button" class="btn btn-secondary add_new_header_field_btn">
<i class="fas fa-plus"></i> Add new header
</button>
</div>
</div>
</div>
<div class="card bg-light mb-3 action-type action-http">
<div class="card-header">
<b>Query parameters</b>
</div>
<div class="card-body">
<h6 class="card-title mb-4">Placeholders are supported in query values.</h6>
<div class="form-group row">
<div class="col-md-12 form_field_http_query_outer">
{{range $idx, $val := .Action.Options.HTTPConfig.QueryParameters}}
<div class="row form_field_http_query_outer_row">
<div class="form-group col-md-5">
<input type="text" class="form-control" id="idHTTPQueryKey{{$idx}}" name="http_query_key{{$idx}}" placeholder="Enter key" value="{{$val.Key}}">
</div>
<div class="form-group col-md-5">
<input type="text" class="form-control" id="idHTTPQueryVal{{$idx}}" name="http_query_val{{$idx}}" placeholder="Enter value" value="{{$val.Value}}">
</div>
<div class="form-group col-md-1"></div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_http_query_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{else}}
<div class="row form_field_http_query_outer_row">
<div class="form-group col-md-5">
<input type="text" class="form-control" id="idHTTPQueryKey0" name="http_query_key0" placeholder="Enter key" value="">
</div>
<div class="form-group col-md-5">
<input type="text" class="form-control" id="idHTTPQueryVal0" name="http_query_val0" placeholder="Enter value" value="">
</div>
<div class="form-group col-md-1"></div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_http_query_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{end}}
</div>
</div>
<div class="row mx-1">
<button type="button" class="btn btn-secondary add_new_query_field_btn">
<i class="fas fa-plus"></i> Add new parameter
</button>
</div>
</div>
</div>
<div class="form-group row action-type action-http">
<label for="idHTTPMethod" class="col-sm-2 col-form-label">Method</label>
<div class="col-sm-10">
<select class="form-control selectpicker" id="idHTTPMethod" name="http_method">
{{- range .HTTPMethods}}
<option value="{{.}}" {{if eq $.Action.Options.HTTPConfig.Method . }}selected{{end}}>{{.}}</option>
{{- end}}
</select>
</div>
</div>
<div class="form-group row action-type action-http">
<label for="idHTTPBody" class="col-sm-2 col-form-label">Body</label>
<div class="col-sm-10">
<textarea class="form-control" id="idHTTPBody" name="http_body" rows="4" placeholder=""
aria-describedby="httpBodyHelpBlock">{{.Action.Options.HTTPConfig.Body}}</textarea>
<small id="httpBodyHelpBlock" class="form-text text-muted">
Placeholders are supported
</small>
</div>
</div>
<div class="form-group row action-type action-http">
<label for="idHTTPTimeout" class="col-sm-2 col-form-label">Timeout</label>
<div class="col-sm-10">
<input type="number" min="1" max="120" class="form-control" id="idHTTPTimeout" name="http_timeout" placeholder=""
value="{{.Action.Options.HTTPConfig.Timeout}}">
</div>
</div>
<div class="form-group action-type action-http">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="idHTTPSkipTLSVerify" name="http_skip_tls_verify"
{{if .Action.Options.HTTPConfig.SkipTLSVerify}}checked{{end}}>
<label for="idHTTPSkipTLSVerify" class="form-check-label">Skip TLS verify</label>
</div>
</div>
<div class="form-group row action-type action-cmd">
<label for="idCmdPath" class="col-sm-2 col-form-label">Command</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idCmdPath" name="cmd_path" placeholder=""
aria-describedby="CmdPathHelpBlock" value="{{.Action.Options.CmdConfig.Cmd}}">
<small id="CmdPathHelpBlock" class="form-text text-muted">
Absolute path of the command to execute
</small>
</div>
</div>
<div class="form-group row action-type action-cmd">
<label for="idCmdTimeout" class="col-sm-2 col-form-label">Timeout</label>
<div class="col-sm-10">
<input type="number" min="1" max="120" class="form-control" id="idCmdTimeout" name="cmd_timeout" placeholder=""
value="{{.Action.Options.CmdConfig.Timeout}}">
</div>
</div>
<div class="card bg-light mb-3 action-type action-cmd">
<div class="card-header">
<b>Environment variables</b>
</div>
<div class="card-body">
<h6 class="card-title mb-4">Placeholders are supported in values.</h6>
<div class="form-group row">
<div class="col-md-12 form_field_cmd_env_outer">
{{range $idx, $val := .Action.Options.CmdConfig.EnvVars}}
<div class="row form_field_cmd_env_outer_row">
<div class="form-group col-md-5">
<input type="text" class="form-control" id="idCMDEnvKey{{$idx}}" name="cmd_env_key{{$idx}}" placeholder="Enter key" value="{{$val.Key}}">
</div>
<div class="form-group col-md-5">
<input type="text" class="form-control" id="idCMDEnvVal{{$idx}}" name="cmd_env_val{{$idx}}" placeholder="Enter value" value="{{$val.Value}}">
</div>
<div class="form-group col-md-1"></div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_cmd_env_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{else}}
<div class="row form_field_cmd_env_outer_row">
<div class="form-group col-md-5">
<input type="text" class="form-control" id="idCMDEnvKey0" name="cmd_env_key0" placeholder="Enter key" value="">
</div>
<div class="form-group col-md-5">
<input type="text" class="form-control" id="idCMDEnvVal0" name="cmd_env_val0" placeholder="Enter value" value="">
</div>
<div class="form-group col-md-1"></div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_cmd_env_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{end}}
</div>
</div>
<div class="row mx-1">
<button type="button" class="btn btn-secondary add_new_cmd_env_field_btn">
<i class="fas fa-plus"></i> Add environment variable
</button>
</div>
</div>
</div>
<div class="form-group row action-type action-smtp">
<label for="idEmailRecipients" class="col-sm-2 col-form-label">Email recipients</label>
<div class="col-sm-10">
<textarea class="form-control" id="idEmailRecipients" name="email_recipients" rows="2" placeholder=""
aria-describedby="smtpRecipientsHelpBlock">{{.Action.Options.EmailConfig.GetRecipientsAsString}}</textarea>
<small id="smtpRecipientsHelpBlock" class="form-text text-muted">
Comma separated email recipients
</small>
</div>
</div>
<div class="form-group row action-type action-smtp">
<label for="idEmailSubject" class="col-sm-2 col-form-label">Email subject</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idEmailSubject" name="email_subject" placeholder=""
value="{{.Action.Options.EmailConfig.Subject}}" maxlength="255" aria-describedby="emailSubjectHelpBlock">
<small id="emailSubjectHelpBlock" class="form-text text-muted">
Placeholders are supported
</small>
</div>
</div>
<div class="form-group row action-type action-smtp">
<label for="idEmailBody" class="col-sm-2 col-form-label">Email body</label>
<div class="col-sm-10">
<textarea class="form-control" id="idEmailBody" name="email_body" rows="4" placeholder=""
aria-describedby="smtpBodyHelpBlock">{{.Action.Options.EmailConfig.Body}}</textarea>
<small id="smtpBodyHelpBlock" class="form-text text-muted">
Placeholders are supported
</small>
</div>
</div>
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<div class="col-sm-12 text-right px-0">
<button type="submit" class="btn btn-primary mt-3 ml-3 px-5" name="form_action" value="submit">Submit</button>
</div>
</form>
</div>
</div>
{{end}}
{{define "dialog"}}
<div class="modal fade" id="infoModal" tabindex="-1" role="dialog" aria-labelledby="infoModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="infoModalLabel">
Supported placeholders
</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>
<span class="shortcut"><b>{{`{{Name}}`}}</b></span> => Username, folder name, or admin username for provider actions.
</p>
<p>
<span class="shortcut"><b>{{`{{Event}}`}}</b></span> => Event name, for example "upload", "download" for filesystem events or "add", "update" for provider events.
</p>
<p>
<span class="shortcut"><b>{{`{{Status}}`}}</b></span> => Status for "upload", "download" and "ssh_cmd" events. 1 means no error, 2 means a generic error occurred, 3 means quota exceeded error.
</p>
<p>
<span class="shortcut"><b>{{`{{VirtualPath}}`}}</b></span> => Path seen by SFTPGo users, for example "/adir/afile.txt".
</p>
<p>
<span class="shortcut"><b>{{`{{FsPath}}`}}</b></span> => Full filesystem path, for example "/user/homedir/adir/afile.txt" or "C:/data/user/homedir/adir/afile.txt" on Windows.
</p>
<p>
<span class="shortcut"><b>{{`{{ObjectName}}`}}</b></span> => File/directory name, for example "afile.txt" or provider object name.
</p>
<p>
<span class="shortcut"><b>{{`{{ObjectType}}`}}</b></span> => Object type for provider events: "user", "group", "admin", etc.
</p>
<p>
<span class="shortcut"><b>{{`{{VirtualTargetPath}}`}}</b></span> => Virtual target path for renames.
</p>
<p>
<span class="shortcut"><b>{{`{{FsTargetPath}}`}}</b></span> => Full filesystem target path for renames.
</p>
<p>
<span class="shortcut"><b>{{`{{FileSize}}`}}</b></span> => File size.
</p>
<p>
<span class="shortcut"><b>{{`{{Protocol}}`}}</b></span> => Protocol, for example "SFTP", "FTP".
</p>
<p>
<span class="shortcut"><b>{{`{{IP}}`}}</b></span> => Client IP address.
</p>
<p>
<span class="shortcut"><b>{{`{{Timestamp}}`}}</b></span> => Event timestamp as nanoseconds since epoch.
</p>
<p>
<span class="shortcut"><b>{{`{{ObjectData}}`}}</b></span> => Provider object data serialized as JSON with sensitive fields removed.
</p>
</div>
<div class="modal-footer">
<button class="btn btn-primary" type="button" data-dismiss="modal">OK</button>
</div>
</div>
</div>
</div>
{{end}}
{{define "extra_js"}}
<script src="{{.StaticURL}}/vendor/bootstrap-select/js/bootstrap-select.min.js"></script>
<script type="text/javascript">
$("body").on("click", ".add_new_header_field_btn", function () {
var index = $(".form_field_http_headers_outer").find(".form_field_http_headers_outer_row").length;
while (document.getElementById("idHTTPHeaderKey"+index) != null){
index++;
}
$(".form_field_http_headers_outer").append(`
<div class="row form_field_http_headers_outer_row">
<div class="form-group col-md-5">
<input type="text" class="form-control" id="idHTTPHeaderKey${index}" name="http_header_key${index}" placeholder="Enter key" value="">
</div>
<div class="form-group col-md-5">
<input type="text" class="form-control" id="idHTTPHeaderVal${index}" name="http_header_val${index}" placeholder="Enter value" value="">
</div>
<div class="form-group col-md-1"></div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_http_header_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`);
});
$("body").on("click", ".remove_http_header_btn_frm_field", function () {
$(this).closest(".form_field_http_headers_outer_row").remove();
});
$("body").on("click", ".add_new_query_field_btn", function () {
var index = $(".form_field_http_query_outer").find(".form_field_http_query_outer_row").length;
while (document.getElementById("idHTTPQueryKey"+index) != null){
index++;
}
$(".form_field_http_query_outer").append(`
<div class="row form_field_http_query_outer_row">
<div class="form-group col-md-5">
<input type="text" class="form-control" id="idHTTPQueryKey${index}" name="http_query_key${index}" placeholder="Enter key" value="">
</div>
<div class="form-group col-md-5">
<input type="text" class="form-control" id="idHTTPQueryVal${index}" name="http_query_val${index}" placeholder="Enter value" value="">
</div>
<div class="form-group col-md-1"></div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_http_query_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`);
});
$("body").on("click", ".remove_http_query_btn_frm_field", function () {
$(this).closest(".form_field_http_query_outer_row").remove();
});
$("body").on("click", ".add_new_cmd_env_field_btn", function () {
var index = $(".form_field_cmd_env_outer").find(".form_field_cmd_env_outer_row").length;
while (document.getElementById("idCMDEnvKey"+index) != null){
index++;
}
$(".form_field_cmd_env_outer").append(`
<div class="row form_field_cmd_env_outer_row">
<div class="form-group col-md-5">
<input type="text" class="form-control" id="idCMDEnvKey${index}" name="cmd_env_key${index}" placeholder="Enter key" value="">
</div>
<div class="form-group col-md-5">
<input type="text" class="form-control" id="idCMDEnvVal${index}" name="cmd_env_val${index}" placeholder="Enter value" value="">
</div>
<div class="form-group col-md-1"></div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_cmd_env_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`);
});
$("body").on("click", ".remove_cmd_env_btn_frm_field", function () {
$(this).closest(".form_field_cmd_env_outer_row").remove();
});
function onTypeChanged(val){
$('.action-type').hide();
switch (val) {
case '1':
case 1:
$('.action-http').show();
break;
case '2':
case 2:
$('.action-cmd').show();
break;
case '3':
case 3:
$('.action-smtp').show();
break;
}
}
$(document).ready(function () {
onTypeChanged('{{.Action.Type}}');
});
</script>
{{end}}

View file

@ -0,0 +1,206 @@
{{template "base" .}}
{{define "title"}}{{.Title}}{{end}}
{{define "extra_css"}}
<link href="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/select.bootstrap4.min.css" rel="stylesheet">
{{end}}
{{define "page_body"}}
<div id="errorMsg" class="card mb-4 border-left-warning" style="display: none;">
<div id="errorTxt" class="card-body text-form-error"></div>
</div>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">View and manage event actions</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover nowrap" id="dataTable" width="100%" cellspacing="0">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Type</th>
<th>Rules</th>
</tr>
</thead>
<tbody>
{{range .Actions}}
<tr>
<td>{{.Name}}</td>
<td>{{.Description}}</td>
<td>{{.GetTypeAsString}}</td>
<td>{{.GetRulesAsString}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
</div>
{{end}}
{{define "dialog"}}
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">
Confirmation required
</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">Do you want to delete the selected event action? A referenced action cannot be removed</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">
Cancel
</button>
<a class="btn btn-warning" href="#" onclick="deleteAction()">
Delete
</a>
</div>
</div>
</div>
</div>
{{end}}
{{define "extra_js"}}
<script src="{{.StaticURL}}/vendor/datatables/jquery.dataTables.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.buttons.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/buttons.colVis.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.fixedHeader.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.responsive.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.select.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/ellipsis.js"></script>
<script type="text/javascript">
function deleteAction() {
var table = $('#dataTable').DataTable();
table.button('delete:name').enable(false);
var name = table.row({ selected: true }).data()[0];
var path = '{{.EventActionURL}}' + "/" + fixedEncodeURIComponent(name);
$('#deleteModal').modal('hide');
$.ajax({
url: path,
type: 'DELETE',
dataType: 'json',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
timeout: 15000,
success: function (result) {
window.location.href = '{{.EventActionsURL}}';
},
error: function ($xhr, textStatus, errorThrown) {
var txt = "Unable to delete the selected action";
if ($xhr) {
var json = $xhr.responseJSON;
if (json) {
if (json.message){
txt += ": " + json.message;
} else {
txt += ": " + json.error;
}
}
}
$('#errorTxt').text(txt);
$('#errorMsg').show();
setTimeout(function () {
$('#errorMsg').hide();
}, 5000);
}
});
}
$(document).ready(function () {
$.fn.dataTable.ext.buttons.add = {
text: '<i class="fas fa-plus"></i>',
name: 'add',
titleAttr: "Add",
action: function (e, dt, node, config) {
window.location.href = '{{.EventActionURL}}';
}
};
$.fn.dataTable.ext.buttons.edit = {
text: '<i class="fas fa-pen"></i>',
name: 'edit',
titleAttr: "Edit",
action: function (e, dt, node, config) {
var name = table.row({ selected: true }).data()[0];
var path = '{{.EventActionURL}}' + "/" + fixedEncodeURIComponent(name);
window.location.href = path;
},
enabled: false
};
$.fn.dataTable.ext.buttons.delete = {
text: '<i class="fas fa-trash"></i>',
name: 'delete',
titleAttr: "Delete",
action: function (e, dt, node, config) {
$('#deleteModal').modal('show');
},
enabled: false
};
var table = $('#dataTable').DataTable({
"select": {
"style": "single",
"blurable": true
},
"stateSave": true,
"stateDuration": 0,
"buttons": [
{
"text": "Column visibility",
"extend": "colvis",
"columns": ":not(.noVis)"
}
],
"columnDefs": [
{
"targets": [0],
"className": "noVis"
},
{
"targets": [3],
"render": $.fn.dataTable.render.ellipsis(100, true)
},
],
"scrollX": false,
"scrollY": false,
"responsive": true,
"language": {
"emptyTable": "No event actions defined"
},
"order": [[0, 'asc']]
});
new $.fn.dataTable.FixedHeader( table );
table.button().add(0,'delete');
table.button().add(0,'edit');
table.button().add(0,'add');
table.buttons().container().appendTo('.col-md-6:eq(0)', table.table().container());
table.on('select deselect', function () {
var selectedRows = table.rows({ selected: true }).count();
table.button('delete:name').enable(selectedRows == 1);
table.button('edit:name').enable(selectedRows == 1);
});
});
</script>
{{end}}

View file

@ -0,0 +1,541 @@
{{template "base" .}}
{{define "title"}}{{.Title}}{{end}}
{{define "extra_css"}}
<link href="{{.StaticURL}}/vendor/bootstrap-select/css/bootstrap-select.min.css" rel="stylesheet">
{{end}}
{{define "page_body"}}
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">{{.Title}}</h6>
</div>
<div class="card-body">
{{if .Error}}
<div class="card mb-4 border-left-warning">
<div class="card-body text-form-error">{{.Error}}</div>
</div>
{{end}}
<form id="eventrule_form" action="{{.CurrentURL}}" method="POST" autocomplete="off">
<div class="form-group row">
<label for="idName" class="col-sm-2 col-form-label">Name</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idName" name="name" placeholder=""
value="{{.Rule.Name}}" maxlength="255" autocomplete="nope" required {{if eq .Mode 2}}readonly{{end}}>
</div>
</div>
<div class="form-group row">
<label for="idDescription" class="col-sm-2 col-form-label">Description</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idDescription" name="description" placeholder=""
value="{{.Rule.Description}}" maxlength="255" aria-describedby="descriptionHelpBlock">
<small id="descriptionHelpBlock" class="form-text text-muted">
Optional description
</small>
</div>
</div>
<div class="form-group row">
<label for="idTrigger" class="col-sm-2 col-form-label">Trigger</label>
<div class="col-sm-10">
<select class="form-control selectpicker" id="idTrigger" name="trigger" onchange="onTriggerChanged(this.value)">
{{- range .TriggerTypes}}
<option value="{{.Value}}" {{if eq $.Rule.Trigger .Value }}selected{{end}}>{{.Name}}</option>
{{- end}}
</select>
</div>
</div>
<div class="form-group row trigger trigger-fs">
<label for="idFsEvents" class="col-sm-2 col-form-label">Fs events</label>
<div class="col-sm-10">
<select class="form-control selectpicker" id="idFsEvents" name="fs_events" multiple>
{{- range $event := .FsEvents}}
<option value="{{$event}}" {{- range $.Rule.Conditions.FsEvents }}{{- if eq . $event}}selected{{- end}}{{- end}}>{{$event}}</option>
{{- end}}
</select>
</div>
</div>
<div class="form-group row trigger trigger-provider">
<label for="idProviderEvents" class="col-sm-2 col-form-label">Provider events</label>
<div class="col-sm-10">
<select class="form-control selectpicker" id="idProviderEvents" name="provider_events" multiple>
{{- range $event := .ProviderEvents}}
<option value="{{$event}}" {{- range $.Rule.Conditions.ProviderEvents }}{{- if eq . $event}}selected{{- end}}{{- end}}>{{$event}}</option>
{{- end}}
</select>
</div>
</div>
<div class="card bg-light mb-3 trigger trigger-schedule">
<div class="card-header">
<b>Schedules</b>
</div>
<div class="card-body">
<h6 class="card-title mb-4">Hours: 0-23. Day of week: 0-6 (Sun-Sat). Day of month: 1-31. Month: 1-12. Asterisk (*) indicates a match for all the values of the field. e.g. every day of week, every day of month and so on. More <a href="https://pkg.go.dev/github.com/robfig/cron/v3#hdr-CRON_Expression_Format" target="_blank">info</a>.</h6>
<div class="form-group row">
<div class="col-md-12 form_field_schedules_outer">
{{range $idx, $val := .Rule.Conditions.Schedules}}
<div class="row form_field_schedules_outer_row">
<div class="form-group col-md-3">
<input type="text" class="form-control" id="idScheduleHour{{$idx}}" name="schedule_hour{{$idx}}" placeholder="Hours" value="{{$val.Hours}}">
</div>
<div class="form-group col-md-3">
<input type="text" class="form-control" id="idScheduleDayOfWeek{{$idx}}" name="schedule_day_of_week{{$idx}}" placeholder="Day of week" value="{{$val.DayOfWeek}}">
</div>
<div class="form-group col-md-3">
<input type="text" class="form-control" id="idScheduleDayOfMonth{{$idx}}" name="schedule_day_of_month{{$idx}}" placeholder="Day of month" value="{{$val.DayOfMonth}}">
</div>
<div class="form-group col-md-2">
<input type="text" class="form-control" id="idScheduleMonth{{$idx}}" name="schedule_month{{$idx}}" placeholder="Month" value="{{$val.Month}}">
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_schedule_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{else}}
<div class="row form_field_schedules_outer_row">
<div class="form-group col-md-3">
<input type="text" class="form-control" id="idScheduleHour0" name="schedule_hour0" placeholder="Hours" value="">
</div>
<div class="form-group col-md-3">
<input type="text" class="form-control" id="idScheduleDayOfWeek0" name="schedule_day_of_week0" placeholder="Day of week" value="">
</div>
<div class="form-group col-md-3">
<input type="text" class="form-control" id="idScheduleDayOfMonth0" name="schedule_day_of_month0" placeholder="Day of month" value="">
</div>
<div class="form-group col-md-2">
<input type="text" class="form-control" id="idScheduleMonth0" name="schedule_month0" placeholder="Month" value="">
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_schedule_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{end}}
</div>
</div>
<div class="row mx-1">
<button type="button" class="btn btn-secondary add_new_schedule_field_btn">
<i class="fas fa-plus"></i> Add new schedule
</button>
</div>
</div>
</div>
{{if .IsShared}}
<div class="form-group trigger trigger-schedule">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="idConcurrentExecution" name="concurrent_execution"
{{if .Rule.Conditions.Options.ConcurrentExecution}}checked{{end}}>
<label for="idConcurrentExecution" class="form-check-label">Allow concurrent execution from multiple instances</label>
</div>
</div>
{{end}}
<div class="form-group row trigger trigger-fs">
<label for="idFsProtocols" class="col-sm-2 col-form-label">Protocol filters</label>
<div class="col-sm-10">
<select class="form-control selectpicker" id="idFsProtocols" name="fs_protocols" aria-describedby="fsProtocolsHelpBlock" multiple>
{{- range $p := .Protocols}}
<option value="{{$p}}" {{- range $.Rule.Conditions.Options.Protocols }}{{- if eq . $p}}selected{{- end}}{{- end}}>{{$p}}</option>
{{- end}}
</select>
<small id="fsProtocolsHelpBlock" class="form-text text-muted">
No selection means any protocol will trigger events
</small>
</div>
</div>
<div class="form-group row trigger trigger-provider">
<label for="idProviderObjects" class="col-sm-2 col-form-label">Object filters</label>
<div class="col-sm-10">
<select class="form-control selectpicker" id="idProviderObjects" name="provider_objects" aria-describedby="providerObjectsHelpBlock" multiple>
{{- range $p := .ProviderObjects}}
<option value="{{$p}}" {{- range $.Rule.Conditions.Options.ProviderObjects }}{{- if eq . $p}}selected{{- end}}{{- end}}>{{$p}}</option>
{{- end}}
</select>
<small id="providerObjectsHelpBlock" class="form-text text-muted">
No selection means any provider object will trigger events
</small>
</div>
</div>
<div class="card bg-light mb-3">
<div class="card-header">
<b>Name filters</b>
</div>
<div class="card-body">
<h6 class="card-title mb-4">Shell-like pattern filters for usernames, folder names. For example "user*"" will match names starting with "user"</h6>
<div class="form-group row">
<div class="col-md-12 form_field_names_outer">
{{range $idx, $val := .Rule.Conditions.Options.Names}}
<div class="row form_field_names_outer_row">
<div class="form-group col-md-8">
<input type="text" class="form-control" id="idNamePattern{{$idx}}" name="name_pattern{{$idx}}" placeholder="" value="{{$val.Pattern}}" maxlength="255">
</div>
<div class="form-group col-md-3">
<select class="form-control selectpicker" id="idNamePatternType{{$idx}}" name="type_name_pattern{{$idx}}">
<option value=""></option>
<option value="inverse" {{if $val.InverseMatch}}selected{{end}}>Inverse match</option>
</select>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_name_pattern_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{else}}
<div class="row form_field_names_outer_row">
<div class="form-group col-md-8">
<input type="text" class="form-control" id="idNamePattern0" name="name_pattern0" placeholder="" value="" maxlength="255">
</div>
<div class="form-group col-md-3">
<select class="form-control selectpicker" id="idNamePatternType0" name="type_name_pattern0">
<option value=""></option>
<option value="inverse">Inverse match</option>
</select>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_name_pattern_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{end}}
</div>
</div>
<div class="row mx-1">
<button type="button" class="btn btn-secondary add_new_name_pattern_field_btn">
<i class="fas fa-plus"></i> Add new filter
</button>
</div>
</div>
</div>
<div class="card bg-light mb-3 trigger trigger-fs">
<div class="card-header">
<b>Path filters</b>
</div>
<div class="card-body">
<h6 class="card-title mb-4">Shell-like pattern filters for filesystem events. For example "/adir/*.txt"" will match paths in the "/adir" directory ending with ".txt"</h6>
<div class="form-group row">
<div class="col-md-12 form_field_fs_paths_outer">
{{range $idx, $val := .Rule.Conditions.Options.FsPaths}}
<div class="row form_field_fs_paths_outer_row">
<div class="form-group col-md-8">
<input type="text" class="form-control" id="idFsPathPattern{{$idx}}" name="fs_path_pattern{{$idx}}" placeholder="" value="{{$val.Pattern}}" maxlength="255">
</div>
<div class="form-group col-md-3">
<select class="form-control selectpicker" id="idFsPathPatternType{{$idx}}" name="type_fs_path_pattern{{$idx}}">
<option value=""></option>
<option value="inverse" {{if $val.InverseMatch}}selected{{end}}>Inverse match</option>
</select>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_fs_path_pattern_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{else}}
<div class="row form_field_fs_paths_outer_row">
<div class="form-group col-md-8">
<input type="text" class="form-control" id="idFsPathPattern0" name="fs_path_pattern0" placeholder="" value="" maxlength="255">
</div>
<div class="form-group col-md-3">
<select class="form-control selectpicker" id="idFsPathPatternType0" name="type_fs_path_pattern0">
<option value=""></option>
<option value="inverse">Inverse match</option>
</select>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_fs_path_pattern_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{end}}
</div>
</div>
<div class="row mx-1">
<button type="button" class="btn btn-secondary add_new_fs_path_pattern_field_btn">
<i class="fas fa-plus"></i> Add new filter
</button>
</div>
</div>
</div>
<div class="card bg-light mb-3 trigger trigger-fs">
<div class="card-header">
<b>File size limits. 0 means no limit</b>
</div>
<div class="card-body">
<div class="form-group row">
<label for="idFsMinSize" class="col-sm-2 col-form-label">Min size</label>
<div class="col-sm-3">
<input type="number" class="form-control" id="idFsMinSize" name="fs_min_size" placeholder=""
value="{{.Rule.Conditions.Options.MinFileSize}}" min="0">
</div>
<div class="col-sm-2"></div>
<label for="idFsMaxSize" class="col-sm-2 col-form-label">Max size</label>
<div class="col-sm-3">
<input type="number" class="form-control" id="idFsMaxSize" name="fs_max_size" placeholder=""
value="{{.Rule.Conditions.Options.MaxFileSize}}" min="0">
</div>
</div>
</div>
</div>
<div class="card bg-light mb-3">
<div class="card-header">
<b>Actions</b>
</div>
<div class="card-body">
<h6 class="card-title mb-4">One or more actions to execute. The "Execute sync" options is only supported for upload events</h6>
<div class="form-group row">
<div class="col-md-12 form_field_action_outer">
{{range $idx, $val := .Rule.Actions}}
<div class="row form_field_action_outer_row">
<div class="form-group col-md-5">
<select class="form-control selectpicker" data-live-search="true" id="idActionName{{$idx}}" name="action_name{{$idx}}">
<option value=""></option>
{{range $.Actions}}
<option value="{{.Name}}" {{if eq $val.Name .Name}}selected{{end}}>{{.Name}}</option>
{{end}}
</select>
</div>
<div class="form-group col-md-5">
<select class="form-control selectpicker" id="idActionOptions{{$idx}}" name="action_options{{$idx}}" multiple>
<option value="2" {{if $val.Options.StopOnFailure}}selected{{end}}>Stop on failure</option>
<option value="3" {{if $val.Options.ExecuteSync}}selected{{end}}>Execute sync</option>
<option value="1" {{if $val.Options.IsFailureAction}}selected{{end}}>Is failure action</option>
</select>
</div>
<div class="col-sm-1">
<input type="hidden" name="action_order{{$idx}}" value="{{$idx}}">
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_action_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{else}}
<div class="row form_field_action_outer_row">
<div class="form-group col-md-5">
<select class="form-control selectpicker" data-live-search="true" id="idActionName0" name="action_name0">
<option value=""></option>
{{range $.Actions}}
<option value="{{.Name}}">{{.Name}}</option>
{{end}}
</select>
</div>
<div class="form-group col-md-5">
<select class="form-control selectpicker" id="idActionOptions0" name="action_options0" multiple>
<option value="1">Is failure action</option>
<option value="2">Stop on failure</option>
<option value="3">Execute sync</option>
</select>
</div>
<div class="col-sm-1">
<input type="hidden" name="action_order0" value="0">
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_action_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{end}}
</div>
</div>
<div class="row mx-1">
<button type="button" class="btn btn-secondary add_new_action_field_btn">
<i class="fas fa-plus"></i> Add new action
</button>
</div>
</div>
</div>
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<div class="col-sm-12 text-right px-0">
<button type="submit" class="btn btn-primary mt-3 ml-3 px-5" name="form_action" value="submit">Submit</button>
</div>
</form>
</div>
</div>
{{end}}
{{define "extra_js"}}
<script src="{{.StaticURL}}/vendor/bootstrap-select/js/bootstrap-select.min.js"></script>
<script type="text/javascript">
$("body").on("click", ".add_new_schedule_field_btn", function () {
var index = $(".form_field_schedules_outer").find(".form_field_schedules_outer_row").length;
while (document.getElementById("idScheduleHour"+index) != null){
index++;
}
$(".form_field_schedules_outer").append(`
<div class="row form_field_schedules_outer_row">
<div class="form-group col-md-3">
<input type="text" class="form-control" id="idScheduleHour${index}" name="schedule_hour${index}" placeholder="Hours" value="">
</div>
<div class="form-group col-md-3">
<input type="text" class="form-control" id="idScheduleDayOfWeek${index}" name="schedule_day_of_week${index}" placeholder="Day of week" value="">
</div>
<div class="form-group col-md-3">
<input type="text" class="form-control" id="idScheduleDayOfMonth${index}" name="schedule_day_of_month${index}" placeholder="Day of month" value="">
</div>
<div class="form-group col-md-2">
<input type="text" class="form-control" id="idScheduleMonth${index}" name="schedule_month${index}" placeholder="Month" value="">
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_schedule_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`);
});
$("body").on("click", ".remove_schedule_btn_frm_field", function () {
$(this).closest(".form_field_schedules_outer_row").remove();
});
$("body").on("click", ".add_new_name_pattern_field_btn", function () {
var index = $(".form_field_names_outer").find(".form_field_names_outer_row").length;
while (document.getElementById("idNamePattern"+index) != null){
index++;
}
$(".form_field_names_outer").append(`
<div class="row form_field_names_outer_row">
<div class="form-group col-md-8">
<input type="text" class="form-control" id="idNamePattern${index}" name="name_pattern${index}" placeholder="" value="" maxlength="255">
</div>
<div class="form-group col-md-3">
<select class="form-control" id="idNamePatternType${index}" name="type_name_pattern${index}">
<option value=""></option>
<option value="inverse">Inverse match</option>
</select>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_name_pattern_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`);
$("#idNamePatternType"+index).selectpicker();
});
$("body").on("click", ".remove_name_pattern_btn_frm_field", function () {
$(this).closest(".form_field_names_outer_row").remove();
});
$("body").on("click", ".add_new_fs_path_pattern_field_btn", function () {
var index = $(".form_field_fs_paths_outer").find("form_field_fs_paths_outer_row").length;
while (document.getElementById("idFsPathPattern"+index) != null){
index++;
}
$(".form_field_fs_paths_outer").append(`
<div class="row form_field_fs_paths_outer_row">
<div class="form-group col-md-8">
<input type="text" class="form-control" id="idFsPathPattern${index}" name="fs_path_pattern${index}" placeholder="" value="" maxlength="255">
</div>
<div class="form-group col-md-3">
<select class="form-control" id="idFsPathPatternType${index}" name="type_fs_path_pattern${index}">
<option value=""></option>
<option value="inverse">Inverse match</option>
</select>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_fs_path_pattern_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`);
$("#idFsPathPatternType"+index).selectpicker();
});
$("body").on("click", ".remove_fs_path_pattern_btn_frm_field", function () {
$(this).closest(".form_field_fs_paths_outer_row").remove();
});
$("body").on("click", ".add_new_action_field_btn", function () {
var index = $(".form_field_action_outer").find("form_field_action_outer_row").length;
while (document.getElementById("idActionName"+index) != null){
index++;
}
$(".form_field_action_outer").append(`
<div class="row form_field_action_outer_row">
<div class="form-group col-md-5">
<select class="form-control" id="idActionName${index}" name="action_name${index}">
<option value=""></option>
</select>
</div>
<div class="form-group col-md-5">
<select class="form-control" id="idActionOptions${index}" name="action_options${index}" multiple>
<option value="1">Is failure action</option>
<option value="2">Stop on failure</option>
<option value="3">Execute sync</option>
</select>
</div>
<div class="col-sm-1">
<input type="hidden" name="action_order${index}" value="${index}">
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_action_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`);
{{- range .Actions}}
$("#idActionName"+index).append($('<option>').val('{{.Name}}').text('{{.Name}}'));
{{- end}}
console.log("index "+index);
$("#idActionName"+index).selectpicker({'liveSearch': true});
$("#idActionOptions"+index).selectpicker();
});
$("body").on("click", ".remove_action_btn_frm_field", function () {
$(this).closest(".form_field_action_outer_row").remove();
});
function onTriggerChanged(val){
$('.trigger').hide();
switch (val) {
case '1':
case 1:
$('.trigger-fs').show();
break;
case '2':
case 2:
$('.trigger-provider').show();
break;
case '3':
case 3:
$('.trigger-schedule').show();
break;
default:
console.log(`unsupported event trigger type: ${val}`);
}
}
$(document).ready(function () {
onTriggerChanged('{{.Rule.Trigger}}');
});
</script>
{{end}}

View file

@ -0,0 +1,206 @@
{{template "base" .}}
{{define "title"}}{{.Title}}{{end}}
{{define "extra_css"}}
<link href="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/select.bootstrap4.min.css" rel="stylesheet">
{{end}}
{{define "page_body"}}
<div id="errorMsg" class="card mb-4 border-left-warning" style="display: none;">
<div id="errorTxt" class="card-body text-form-error"></div>
</div>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">View and manage event rules</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover nowrap" id="dataTable" width="100%" cellspacing="0">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Trigger</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{range .Rules}}
<tr>
<td>{{.Name}}</td>
<td>{{.Description}}</td>
<td>{{.GetTriggerAsString}}</td>
<td>{{.GetActionsAsString}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
</div>
{{end}}
{{define "dialog"}}
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">
Confirmation required
</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">Do you want to delete the selected rule?</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">
Cancel
</button>
<a class="btn btn-warning" href="#" onclick="deleteAction()">
Delete
</a>
</div>
</div>
</div>
</div>
{{end}}
{{define "extra_js"}}
<script src="{{.StaticURL}}/vendor/datatables/jquery.dataTables.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.buttons.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/buttons.colVis.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.fixedHeader.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.responsive.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.select.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/ellipsis.js"></script>
<script type="text/javascript">
function deleteAction() {
var table = $('#dataTable').DataTable();
table.button('delete:name').enable(false);
var name = table.row({ selected: true }).data()[0];
var path = '{{.EventRuleURL}}' + "/" + fixedEncodeURIComponent(name);
$('#deleteModal').modal('hide');
$.ajax({
url: path,
type: 'DELETE',
dataType: 'json',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
timeout: 15000,
success: function (result) {
window.location.href = '{{.EventRulesURL}}';
},
error: function ($xhr, textStatus, errorThrown) {
var txt = "Unable to delete the selected rule";
if ($xhr) {
var json = $xhr.responseJSON;
if (json) {
if (json.message){
txt += ": " + json.message;
} else {
txt += ": " + json.error;
}
}
}
$('#errorTxt').text(txt);
$('#errorMsg').show();
setTimeout(function () {
$('#errorMsg').hide();
}, 5000);
}
});
}
$(document).ready(function () {
$.fn.dataTable.ext.buttons.add = {
text: '<i class="fas fa-plus"></i>',
name: 'add',
titleAttr: "Add",
action: function (e, dt, node, config) {
window.location.href = '{{.EventRuleURL}}';
}
};
$.fn.dataTable.ext.buttons.edit = {
text: '<i class="fas fa-pen"></i>',
name: 'edit',
titleAttr: "Edit",
action: function (e, dt, node, config) {
var name = table.row({ selected: true }).data()[0];
var path = '{{.EventRuleURL}}' + "/" + fixedEncodeURIComponent(name);
window.location.href = path;
},
enabled: false
};
$.fn.dataTable.ext.buttons.delete = {
text: '<i class="fas fa-trash"></i>',
name: 'delete',
titleAttr: "Delete",
action: function (e, dt, node, config) {
$('#deleteModal').modal('show');
},
enabled: false
};
var table = $('#dataTable').DataTable({
"select": {
"style": "single",
"blurable": true
},
"stateSave": true,
"stateDuration": 0,
"buttons": [
{
"text": "Column visibility",
"extend": "colvis",
"columns": ":not(.noVis)"
}
],
"columnDefs": [
{
"targets": [0],
"className": "noVis"
},
{
"targets": [3],
"render": $.fn.dataTable.render.ellipsis(100, true)
},
],
"scrollX": false,
"scrollY": false,
"responsive": true,
"language": {
"emptyTable": "No event rules defined"
},
"order": [[0, 'asc']]
});
new $.fn.dataTable.FixedHeader( table );
table.button().add(0,'delete');
table.button().add(0,'edit');
table.button().add(0,'add');
table.buttons().container().appendTo('.col-md-6:eq(0)', table.table().container());
table.on('select deselect', function () {
var selectedRows = table.rows({ selected: true }).count();
table.button('delete:name').enable(selectedRows == 1);
table.button('edit:name').enable(selectedRows == 1);
});
});
</script>
{{end}}

View file

@ -15,10 +15,6 @@
<div id="errorTxt" class="card-body text-form-error"></div> <div id="errorTxt" class="card-body text-form-error"></div>
</div> </div>
<div id="successMsg" class="card mb-4 border-left-success" style="display: none;">
<div id="successTxt" class="card-body"></div>
</div>
<div class="card shadow mb-4"> <div class="card shadow mb-4">
<div class="card-header py-3"> <div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">View and manage groups</h6> <h6 class="m-0 font-weight-bold text-primary">View and manage groups</h6>

View file

@ -78,154 +78,154 @@ CzgWkxiz7XE4lgUwX44FCXZM3+JeUbI=
-----END EC PRIVATE KEY-----` -----END EC PRIVATE KEY-----`
caCRT = `-----BEGIN CERTIFICATE----- caCRT = `-----BEGIN CERTIFICATE-----
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
r3rwjFsQOoZotA== WwSa13hh6yjoDQ==
-----END CERTIFICATE-----` -----END CERTIFICATE-----`
caCRL = `-----BEGIN X509 CRL----- caCRL = `-----BEGIN X509 CRL-----
MIICpzCBkAIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0QXV0aBcN MIICpzCBkAIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0QXV0aBcN
MjEwMTAyMjEzNDA1WhcNMjMwMTAyMjEzNDA1WjAkMCICEQC+l04DbHWMyC3fG09k MjIwNzA0MTU1MzU4WhcNMjQwNzAzMTU1MzU4WjAkMCICEQDZo5Q3lhxFuDUsxGNm
VXf+Fw0yMTAxMDIyMTM0MDVaoCMwITAfBgNVHSMEGDAWgBTtcgjQBq/80EySIvJc 794YFw0yMjA3MDQxNTUzNThaoCMwITAfBgNVHSMEGDAWgBT4qzwTztNc1Ln+8Qvn
N8OTorgbzDANBgkqhkiG9w0BAQsFAAOCAgEAEJ7z+uNc8sqtxlOhSdTGDzX/xput bGzjmOULwDANBgkqhkiG9w0BAQsFAAOCAgEA1lK6g8qmhyY6myx8342dDuaauY03
E857kFQkSlMnU2whQ8c+XpYrBLA5vIZJNSSwohTpM4+zVBX/bJpmu3wqqaArRO9/ 0iojkxpasuYcytK6XRm96YqjZK9EETxsHHViVU0vCXES60D6wJ9gw4fTWn3WxEdx
YcW5mQk9Anvb4WjQW1cHmtNapMTzoC9AiYt/OWPfy+P6JCgCr4Hy6LgQyIRL6bM9 nIwbGyjUGHh2y+R3uQsfvwxsdYvDsTLAnOLwOo68dAHWmMDZRmgTuGNoYFxVQRGR
VYTalolOm1qa4Y5cIeT7iHq/91mfaqo8/6MYRjLl8DOTROpmw8OS9bCXkzGKdCat Cn90ZR7LPLpCScclWM8FE/W1B90x3ZE8EhJiCI/WyyTh3EgshmB7A5GoDrFZfmvR
AbAzwkQUSauyoCQ10rpX+Y64w9ng3g4Dr20aCqPf5osaqplEJ2HTK8ljDTidlslv dzoTKO+F9p2XjtmgfiBE3czWQysfATmbutZUbG/ZRb89u+ZEUyPoC94mg8fhNWoX
9anQj8ax3Su89vI8+hK+YbfVQwrThabgdSjQsn+veyx8GlP8WwHLAQ379KjZjWg+ 1d5G9QAkZFHp957/5QHLq9OHNfnWXoohhebjF4VWqZH7w+RtLc8t0PIog2lX4t1o
OlOSwBeU1vTdP0QcB8X5C2gVujAyuQekbaV86xzIBOj7vZdfHZ6ee30TZ2FKiMyg 5N/xFk9akvuoyNGg/fYuJBmN162Q0MdeYfYKDGWdXxf6fpHxVr5v2JrIx6gOwubb
7/N2OqW0w77ChsjB4MSHJCfuTgIeg62GzuZXLM+Q2Z9LBdtm4Byg+sm/P52adOEg cIKP22ZBv/PYOeFsAZ755lTl4OTFUjU5ZJEPD6pUc1daaIqfxsxu8gDZP92FZjsB
gVb2Zf4KSvsAmA0PIBlu449/QXUFcMxzLFy7mwTeZj2B4Ln0Hm0szV9f9R8MwMtB zaalMbh30n2OhagSMBzSLg5rE6WmBzlQX0ZN8YrW4l2Vq6twnnFHY+UyblRZS+d4
SyLYxVH+mgqaR6Jkk22Q/yYyLPaELfafX5gp/AIXG8n0zxfVaTvK3auSgb1Q6ZLS oHBaoOaxPEkLxNZ8ulzJS4B6c4D1CXOaBEf++snVzRRUOEdX3x7TvkkrLvIsm06R
5QH9dSIsmZHlPq7GoSXmKpMdjUL8eaky/IMteioyXgsBiATzl5L2dsw6MTX3MDF0 ux0L1zJb9LbZ/1rhuv70z/kIlD55sqYuRqu3RpgTgZuTERU//rYIqWd03Y5Qon8i
QbDK+MzhmbKfDxs= VoC6Yp9DPldQJrk=
-----END X509 CRL-----` -----END X509 CRL-----`
client1Crt = `-----BEGIN CERTIFICATE----- client1Crt = `-----BEGIN CERTIFICATE-----
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
O/e3EH8= 7LEmxYg=
-----END CERTIFICATE-----` -----END CERTIFICATE-----`
client1Key = `-----BEGIN RSA PRIVATE KEY----- client1Key = `-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAoKbYY9MdF2kF/nhBESIiZTdVYtA8XL9xrIZyDj9EnCiTxHiV MIIEpAIBAAKCAQEA8xM5v+2QfdzfwnNT5cl+6oEy2fZoI2YG6L6c25rG0pr+yl1I
bJtHXVwszqSl5TRrotPmnmAQcX3r8OCk+z+RQZ0QQj257P3kG6q4rNnOcWCS5xEd HKdMZcvn93uat7hlbzxeOLfJRM7+QK1lLaxuppq9p+gT+1x9eG3E4X7e0pdbjrpJ
20jPyhQ3m+hMGfZsotNTQze1ochuQgLUN6IPyPxZkH22ia3jX4iu1eo/QxeLYHj1 GbvNji0hwDBLDWD8mHNq/SCk9FKtGnfZqrNB5BLw2uIKjJzVGXVlsjN6geBDm2hV
UHw43Cii9yE+j5kPUC21xmnrGKdUrB55NYLXHx6yTIqYR5znSOVB8oJi18/hwdZm jTSmzMr39CfLUdtvMaZhpIPJzbH+sNfp1zKavFIpmwCd77p/z0QAiQ9NaIvzv4PZ
H859DHhm0Hx1HrS+jbjI3+CMorZJ3WUyNf+CkiVLD3xYutPbxzEpwiqkG/XYzLH0 DDEEMUHzmVAU6bUjD8GToXaMbRiz694SU8aAwvvcdjGexdbHnfSAfLOl2wTPPxve
habTcDcILo18n+o3jvem2KWBrDhyairjIDscwQIDAQABAoIBAEBSjVFqtbsp0byR PncRaa656ZeZWxY9pRCItP+v43nm7d4sAyRD4QIDAQABAoIBADE17zcgDWSt1s8z
aXvyrtLX1Ng7h++at2jca85Ihq//jyqbHTje8zPuNAKI6eNbmb0YGr5OuEa4pD9N MgUPahZn2beu3x5rhXKRRIhhKWdx4atufy7t39WsFmZQK96OAlsmyZyJ+MFpdqf5
ssDmMsKSoG/lRwwcm7h4InkSvBWpFShvMgUaohfHAHzsBYxfnh+TfULsi0y7c2n6 csZwZmZsZYEcxw7Yhr5e2sEcQlg4NF0M8ce38cGa+X5DSK6IuBrVIw/kEAE2y7zU
t/2OZcOTRkkUDIITnXYiw93ibHHv2Mv2bBDu35kGrcK+c2dN5IL5ZjTjMRpbJTe2 Dsk0SV63RvPJV4FoLuxcjB4rtd2c+JBduNUXQYVppz/KhsXN+9CbPbZ7wo1cB5fo
44RBJbdTxHBVSgoGBnugF+s2aEma6Ehsj70oyfoVpM6Aed5kGge0A5zA1JO7WCn9 Iu/VswvvW6EAxVx39zZcwSGdkss9XUktU8akx7T/pepIH6fwkm7uXSNez6GH9d1I
Ay/DzlULRXHjJIoRWd2NKvx5n3FNppUc9vJh2plRHalRooZ2+MjSf8HmXlvG2Hpb 8qOiORk/gAtqPL1TJgConyYheWMM9RbXP/IwL0BV8U4ZVG53S8jx2XpP4OJQ+k35
ScvmWgECgYEA1G+A/2KnxWsr/7uWIJ7ClcGCiNLdk17Pv3DZ3G4qUsU2ITftfIbb WYvz8JECgYEA+9OywKOG2lMiiUB1qZfmXB80PngNsz+L6xUWkrw58gSqYZIg0xyH
tU0Q/b19na1IY8Pjy9ptP7t74/hF5kky97cf1FA8F+nMj/k4+wO8QDI8OJfzVzh9 Sfr7HBo0yn/PB0oMMWPpNfYvG8/kSMIWiVlsYz9fdsUuqIvN+Kh9VF6o2wn+gnJk
PwielA5vbE+xmvis5Hdp8/od1Yrc/rPSy2TKtPFhvsqXjqoUmOAjDP8CgYEAwZjH sBE3KVMofcgwgLE6eMVv2MSQlBoXhGPNlCBHS1gorQdYE82dxDPBBzsCgYEA9xpm
9dt1sc2lx/rMxihlWEzQ3JPswKW9/LJAmbRBoSWF9FGNjbX7uhWtXRKJkzb8ZAwa c3C9LxiVbw9ZZ5D2C+vzwIG2+ZeDwKSizM1436MAnzNQgQTMzQ20uFGNBD562VjI
88azluNo2oftbDD/+jw8b2cDgaJHlLAkSD4O1D1RthW7/LKD15qZ/oFsRb13NV85 rHFlZYr3KCtSIw5gvCSuox0YB64Yq/WAtGZtH9JyKRz4h4juq6iM4FT7nUwM4DF9
ZNKtwslXGbfVNyGKUVFm7fVA8vBAOUey+LKDFj8CgYEAg8WWstOzVdYguMTXXuyb 3CUiDS8DGoqvCNpY50GvzSR5QVT1DKTZsMunh5MCgYEAyIWMq7pK0iQqtvG9/3o1
ruEV42FJaDyLiSirOvxq7GTAKuLSQUg1yMRBIeQEo2X1XU0JZE3dLodRVhuO4EXP 8xrhxfBgsF+kcV+MZvE8jstKRIFQY+oujCkutPTlHm3hE2PSC64L8G0Em/fRRmJO
g7Dn4X7Th9HSvgvNuIacowWGLWSz4Qp9RjhGhXhezUSx2nseY6le46PmFavJYYSR AbZUCT9YK8HdYlZYf2zix0DM4gW2RHcEV/KNYvmVn3q9rGvzLGHCqu/yVAvmuAOk
4PBofMyt4PcyA6Cknh+KHmkCgYEAnTriG7ETE0a7v4DXUpB4TpCEiMCy5Xs2o8Z5 mhON0Z/0W7siVjp/KtEvHisCgYA/cfTaMRkyDXLY6C0BbXPvTa7xP5z2atO2U89F
ZNva+W+qLVUWq+MDAIyechqeFSvxK6gRM69LJ96lx+XhU58wJiFJzAhT9rK/g+jS HICrkxOmzKsf5VacU6eSJ8Y4T76FLcmglSD+uHaLRsw5Ggj2Zci9MswntKi7Bjb8
bsHH9WOfu0xHkuHA5hgvvV2Le9B2wqgFyva4HJy82qxMxCu/VG/SMqyfBS9OWbb7 msvr/sG3EqwxSJRXWNiLBObx1UP9EFgLfTFIB0kZuIAGmuF2xyPXXUUQ5Dpi+7S1
ibQhdq0CgYAl53LUWZsFSZIth1vux2LVOsI8C3X1oiXDGpnrdlQ+K7z57hq5EsRq MyUZpwKBgQDg+AIPvk41vQ4Cz2CKrQX5/uJSW4bOhgP1yk7ruIH4Djkag3ZzTnHM
GC+INxwXbvKNqp5h0z2MvmKYPDlGVTgw8f8JjM7TkN17ERLcydhdRrMONUryZpo8 zA9/pLzRfz1ENc5I/WaYSh92eKw3j6tUtMJlE2AbfCpgOQtRUNs3IBmzCWrY8J01
1xTob+8blyJgfxZUIAKbMbMbIiU0WAF0rfD/eJJwS4htOW/Hfv4TGA== W/8bwB+KhfFxNYwvszYsvvOq51NgahYQkgThVm38UixB3PFpEf+NiQ==
-----END RSA PRIVATE KEY-----` -----END RSA PRIVATE KEY-----`
// client 2 crt is revoked // client 2 crt is revoked
client2Crt = `-----BEGIN CERTIFICATE----- client2Crt = `-----BEGIN CERTIFICATE-----
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
6zdDidU= enqNrFI=
-----END CERTIFICATE-----` -----END CERTIFICATE-----`
client2Key = `-----BEGIN RSA PRIVATE KEY----- client2Key = `-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----` -----END RSA PRIVATE KEY-----`
testFileName = "test_file_dav.dat" testFileName = "test_file_dav.dat"
testDLFileName = "test_download_dav.dat" testDLFileName = "test_download_dav.dat"