mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-21 23:20:24 +00:00
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:
parent
e46051299f
commit
1b8f94c08f
54 changed files with 10238 additions and 869 deletions
|
@ -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.
|
||||
- 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.
|
||||
- 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 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.
|
||||
|
|
|
@ -34,7 +34,7 @@ If the SMTP configuration is correct you should receive this email.`,
|
|||
logger.ErrorToConsole("unable to initialize SMTP configuration: %v", err)
|
||||
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)
|
||||
if err != nil {
|
||||
logger.WarnToConsole("Error sending email: %v", err)
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -87,7 +88,8 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua
|
|||
) error {
|
||||
hasNotifiersPlugin := plugin.Handler.HasNotifiers()
|
||||
hasHook := util.Contains(Config.Actions.ExecuteOn, operation)
|
||||
if !hasHook && !hasNotifiersPlugin {
|
||||
hasRules := dataprovider.EventManager.HasFsRules()
|
||||
if !hasHook && !hasNotifiersPlugin && !hasRules {
|
||||
return nil
|
||||
}
|
||||
notification := newActionNotification(&conn.User, operation, filePath, virtualPath, target, virtualTarget, sshCmd,
|
||||
|
@ -95,15 +97,34 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua
|
|||
if hasNotifiersPlugin {
|
||||
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 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
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return errRes
|
||||
}
|
||||
|
||||
// ActionHandler handles a notification for a Protocol Action.
|
||||
|
@ -119,7 +140,6 @@ func newActionNotification(
|
|||
err error,
|
||||
) *notifier.FsEvent {
|
||||
var bucket, endpoint string
|
||||
status := 1
|
||||
|
||||
fsConfig := user.GetFsConfigForPath(virtualPath)
|
||||
|
||||
|
@ -140,12 +160,6 @@ func newActionNotification(
|
|||
endpoint = fsConfig.HTTPConfig.Endpoint
|
||||
}
|
||||
|
||||
if err == ErrQuotaExceeded {
|
||||
status = 3
|
||||
} else if err != nil {
|
||||
status = 2
|
||||
}
|
||||
|
||||
return ¬ifier.FsEvent{
|
||||
Action: operation,
|
||||
Username: user.Username,
|
||||
|
@ -158,7 +172,7 @@ func newActionNotification(
|
|||
FsProvider: int(fsConfig.Provider),
|
||||
Bucket: bucket,
|
||||
Endpoint: endpoint,
|
||||
Status: status,
|
||||
Status: getNotificationStatus(err),
|
||||
Protocol: protocol,
|
||||
IP: ip,
|
||||
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)
|
||||
|
||||
return err
|
||||
|
@ -243,22 +257,32 @@ func (h *defaultActionHandler) handleCommand(event *notifier.FsEvent) error {
|
|||
|
||||
func notificationAsEnvVars(event *notifier.FsEvent) []string {
|
||||
return []string{
|
||||
fmt.Sprintf("SFTPGO_ACTION=%v", event.Action),
|
||||
fmt.Sprintf("SFTPGO_ACTION_USERNAME=%v", event.Username),
|
||||
fmt.Sprintf("SFTPGO_ACTION_PATH=%v", event.Path),
|
||||
fmt.Sprintf("SFTPGO_ACTION_TARGET=%v", event.TargetPath),
|
||||
fmt.Sprintf("SFTPGO_ACTION_VIRTUAL_PATH=%v", event.VirtualPath),
|
||||
fmt.Sprintf("SFTPGO_ACTION_VIRTUAL_TARGET=%v", event.VirtualTargetPath),
|
||||
fmt.Sprintf("SFTPGO_ACTION_SSH_CMD=%v", event.SSHCmd),
|
||||
fmt.Sprintf("SFTPGO_ACTION_FILE_SIZE=%v", event.FileSize),
|
||||
fmt.Sprintf("SFTPGO_ACTION_FS_PROVIDER=%v", event.FsProvider),
|
||||
fmt.Sprintf("SFTPGO_ACTION_BUCKET=%v", event.Bucket),
|
||||
fmt.Sprintf("SFTPGO_ACTION_ENDPOINT=%v", event.Endpoint),
|
||||
fmt.Sprintf("SFTPGO_ACTION_STATUS=%v", event.Status),
|
||||
fmt.Sprintf("SFTPGO_ACTION_PROTOCOL=%v", event.Protocol),
|
||||
fmt.Sprintf("SFTPGO_ACTION_IP=%v", event.IP),
|
||||
fmt.Sprintf("SFTPGO_ACTION_SESSION_ID=%v", event.SessionID),
|
||||
fmt.Sprintf("SFTPGO_ACTION_OPEN_FLAGS=%v", event.OpenFlags),
|
||||
fmt.Sprintf("SFTPGO_ACTION_TIMESTAMP=%v", event.Timestamp),
|
||||
fmt.Sprintf("SFTPGO_ACTION=%s", event.Action),
|
||||
fmt.Sprintf("SFTPGO_ACTION_USERNAME=%s", event.Username),
|
||||
fmt.Sprintf("SFTPGO_ACTION_PATH=%s", event.Path),
|
||||
fmt.Sprintf("SFTPGO_ACTION_TARGET=%s", event.TargetPath),
|
||||
fmt.Sprintf("SFTPGO_ACTION_VIRTUAL_PATH=%s", event.VirtualPath),
|
||||
fmt.Sprintf("SFTPGO_ACTION_VIRTUAL_TARGET=%s", event.VirtualTargetPath),
|
||||
fmt.Sprintf("SFTPGO_ACTION_SSH_CMD=%s", event.SSHCmd),
|
||||
fmt.Sprintf("SFTPGO_ACTION_FILE_SIZE=%d", event.FileSize),
|
||||
fmt.Sprintf("SFTPGO_ACTION_FS_PROVIDER=%d", event.FsProvider),
|
||||
fmt.Sprintf("SFTPGO_ACTION_BUCKET=%s", event.Bucket),
|
||||
fmt.Sprintf("SFTPGO_ACTION_ENDPOINT=%s", event.Endpoint),
|
||||
fmt.Sprintf("SFTPGO_ACTION_STATUS=%d", event.Status),
|
||||
fmt.Sprintf("SFTPGO_ACTION_PROTOCOL=%s", event.Protocol),
|
||||
fmt.Sprintf("SFTPGO_ACTION_IP=%s", event.IP),
|
||||
fmt.Sprintf("SFTPGO_ACTION_SESSION_ID=%s", event.SessionID),
|
||||
fmt.Sprintf("SFTPGO_ACTION_OPEN_FLAGS=%d", event.OpenFlags),
|
||||
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
|
||||
}
|
||||
|
|
120
common/common.go
120
common/common.go
|
@ -125,8 +125,6 @@ var (
|
|||
Config Configuration
|
||||
// Connections is the list of active connections
|
||||
Connections ActiveConnections
|
||||
// QuotaScans is the list of active quota scans
|
||||
QuotaScans ActiveScans
|
||||
transfersChecker TransfersChecker
|
||||
periodicTimeoutTicker *time.Ticker
|
||||
periodicTimeoutTickerDone chan bool
|
||||
|
@ -396,11 +394,11 @@ func (t *ConnectionTransfer) getConnectionTransferAsString() string {
|
|||
case operationDownload:
|
||||
result += "DL "
|
||||
}
|
||||
result += fmt.Sprintf("%#v ", t.VirtualPath)
|
||||
result += fmt.Sprintf("%q ", t.VirtualPath)
|
||||
if t.Size > 0 {
|
||||
elapsed := time.Since(util.GetTimeFromMsecSinceEpoch(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)
|
||||
}
|
||||
return result
|
||||
|
@ -1150,117 +1148,3 @@ func (c *ConnectionStatus) GetTransfersAsString() string {
|
|||
}
|
||||
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
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
@ -701,35 +702,6 @@ func TestConnectionStatus(t *testing.T) {
|
|||
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) {
|
||||
c := Configuration{
|
||||
ProxyProtocol: 0,
|
||||
|
@ -1044,6 +1016,110 @@ func TestUserRecentActivity(t *testing.T) {
|
|||
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) {
|
||||
bcryptPassword := "bcryptpassword"
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
|
|
@ -35,11 +35,11 @@ const (
|
|||
)
|
||||
|
||||
var (
|
||||
// RetentionChecks is the list of active quota scans
|
||||
// RetentionChecks is the list of active retention checks
|
||||
RetentionChecks ActiveRetentionChecks
|
||||
)
|
||||
|
||||
// ActiveRetentionChecks holds the active quota scans
|
||||
// ActiveRetentionChecks holds the active retention checks
|
||||
type ActiveRetentionChecks struct {
|
||||
sync.RWMutex
|
||||
Checks []RetentionCheck
|
||||
|
@ -390,7 +390,7 @@ func (c *RetentionCheck) sendEmailNotification(elapsed time.Duration, errCheck e
|
|||
}
|
||||
startTime := time.Now()
|
||||
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,
|
||||
time.Since(startTime))
|
||||
return err
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -39,6 +40,7 @@ import (
|
|||
"github.com/drakkan/sftpgo/v2/kms"
|
||||
"github.com/drakkan/sftpgo/v2/logger"
|
||||
"github.com/drakkan/sftpgo/v2/mfa"
|
||||
"github.com/drakkan/sftpgo/v2/smtp"
|
||||
"github.com/drakkan/sftpgo/v2/util"
|
||||
"github.com/drakkan/sftpgo/v2/vfs"
|
||||
)
|
||||
|
@ -62,6 +64,7 @@ var (
|
|||
homeBasePath string
|
||||
logFilePath string
|
||||
testFileContent = []byte("test data")
|
||||
lastReceivedEmail receivedEmail
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
@ -172,6 +175,7 @@ func TestMain(m *testing.M) {
|
|||
|
||||
go func() {
|
||||
if err := smtpd.ListenAndServe(smtpServerAddr, func(remoteAddr net.Addr, from string, to []string, data []byte) error {
|
||||
lastReceivedEmail.set(from, to, data)
|
||||
return nil
|
||||
}, "SFTPGo test", "localhost"); err != nil {
|
||||
logger.ErrorToConsole("could not start SMTP server: %v", err)
|
||||
|
@ -2799,6 +2803,279 @@ func TestPasswordCaching(t *testing.T) {
|
|||
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) {
|
||||
if runtime.GOOS == osWindows {
|
||||
t.Skip("this test is not available on Windows")
|
||||
|
@ -3865,3 +4142,39 @@ func printLatestLogs(maxNumberOfLines int) {
|
|||
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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -342,11 +342,6 @@ func Init() {
|
|||
NamingRules: 1,
|
||||
IsShared: 0,
|
||||
BackupsPath: "backups",
|
||||
AutoBackup: dataprovider.AutoBackup{
|
||||
Enabled: true,
|
||||
Hour: "0",
|
||||
DayOfWeek: "*",
|
||||
},
|
||||
},
|
||||
HTTPDConfig: httpd.Conf{
|
||||
Bindings: []httpd.Binding{defaultHTTPDBinding},
|
||||
|
@ -1919,9 +1914,6 @@ func setViperDefaults() {
|
|||
viper.SetDefault("data_provider.naming_rules", globalConf.ProviderConf.NamingRules)
|
||||
viper.SetDefault("data_provider.is_shared", globalConf.ProviderConf.IsShared)
|
||||
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.static_files_path", globalConf.HTTPDConfig.StaticFilesPath)
|
||||
viper.SetDefault("httpd.openapi_path", globalConf.HTTPDConfig.OpenAPIPath)
|
||||
|
|
|
@ -33,6 +33,8 @@ const (
|
|||
actionObjectAdmin = "admin"
|
||||
actionObjectAPIKey = "api_key"
|
||||
actionObjectShare = "share"
|
||||
actionObjectEventAction = "event_action"
|
||||
actionObjectEventRule = "event_rule"
|
||||
)
|
||||
|
||||
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(),
|
||||
}, 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 == "" {
|
||||
return
|
||||
}
|
||||
|
@ -74,7 +88,7 @@ func executeAction(operation, executor, ip, objectType, objectName string, objec
|
|||
q.Add("ip", ip)
|
||||
q.Add("object_type", objectType)
|
||||
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()
|
||||
startTime := time.Now()
|
||||
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.Env = append(env,
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_ACTION=%v", operation),
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_OBJECT_TYPE=%v", objectType),
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_OBJECT_NAME=%v", objectName),
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_USERNAME=%v", executor),
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_IP=%v", ip),
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_TIMESTAMP=%v", util.GetTimeAsMsSinceEpoch(time.Now())),
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_OBJECT=%v", string(objectAsJSON)))
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_ACTION=%vs", operation),
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_OBJECT_TYPE=%s", objectType),
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_OBJECT_NAME=%s", objectName),
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_USERNAME=%s", executor),
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_IP=%s", ip),
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_TIMESTAMP=%d", util.GetTimeAsMsSinceEpoch(time.Now())),
|
||||
fmt.Sprintf("SFTPGO_PROVIDER_OBJECT=%s", string(objectAsJSON)))
|
||||
|
||||
startTime := time.Now()
|
||||
err := cmd.Run()
|
||||
|
|
|
@ -40,6 +40,7 @@ const (
|
|||
PermAdminRetentionChecks = "retention_checks"
|
||||
PermAdminMetadataChecks = "metadata_checks"
|
||||
PermAdminViewEvents = "view_events"
|
||||
PermAdminManageEventRules = "manage_event_rules"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
bolt "go.etcd.io/bbolt"
|
||||
|
@ -20,7 +21,7 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
boltDatabaseVersion = 19
|
||||
boltDatabaseVersion = 20
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -30,10 +31,12 @@ var (
|
|||
adminsBucket = []byte("admins")
|
||||
apiKeysBucket = []byte("api_keys")
|
||||
sharesBucket = []byte("shares")
|
||||
actionsBucket = []byte("events_actions")
|
||||
rulesBucket = []byte("events_rules")
|
||||
dbVersionBucket = []byte("db_version")
|
||||
dbVersionKey = []byte("version")
|
||||
boltBuckets = [][]byte{usersBucket, groupsBucket, foldersBucket, adminsBucket, apiKeysBucket,
|
||||
sharesBucket, dbVersionBucket}
|
||||
sharesBucket, actionsBucket, rulesBucket, dbVersionBucket}
|
||||
)
|
||||
|
||||
// BoltProvider defines the auth provider for bolt key/value store
|
||||
|
@ -629,30 +632,35 @@ func (p *BoltProvider) deleteUser(user User) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
exists := bucket.Get([]byte(user.Username))
|
||||
if exists == nil {
|
||||
return util.NewRecordNotFoundError(fmt.Sprintf("user %#v does not exist", user.Username))
|
||||
var u []byte
|
||||
if u = bucket.Get([]byte(user.Username)); u == nil {
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for idx := range user.VirtualFolders {
|
||||
err = p.removeRelationFromFolderMapping(user.VirtualFolders[idx], user.Username, "", foldersBucket)
|
||||
for idx := range oldUser.VirtualFolders {
|
||||
err = p.removeRelationFromFolderMapping(oldUser.VirtualFolders[idx], oldUser.Username, "", foldersBucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(user.Groups) > 0 {
|
||||
if len(oldUser.Groups) > 0 {
|
||||
groupBucket, err := p.getGroupsBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for idx := range user.Groups {
|
||||
err = p.removeUserFromGroupMapping(user.Username, user.Groups[idx].Name, groupBucket)
|
||||
for idx := range oldUser.Groups {
|
||||
err = p.removeUserFromGroupMapping(oldUser.Username, oldUser.Groups[idx].Name, groupBucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1362,6 +1370,7 @@ func (p *BoltProvider) updateGroup(group *Group) error {
|
|||
}
|
||||
group.ID = oldGroup.ID
|
||||
group.CreatedAt = oldGroup.CreatedAt
|
||||
group.Users = oldGroup.Users
|
||||
group.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
buf, err := json.Marshal(group)
|
||||
if err != nil {
|
||||
|
@ -1932,6 +1941,490 @@ func (p *BoltProvider) cleanupSharedSessions(sessionType SessionType, before int
|
|||
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 {
|
||||
return p.dbHandle.Close()
|
||||
}
|
||||
|
@ -1959,6 +2452,10 @@ func (p *BoltProvider) migrateDatabase() error {
|
|||
providerLog(logger.LevelError, "%v", err)
|
||||
logger.ErrorToConsole("%v", 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:
|
||||
if version > boltDatabaseVersion {
|
||||
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")
|
||||
}
|
||||
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:
|
||||
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) {
|
||||
var group 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)
|
||||
}
|
||||
|
||||
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 {
|
||||
g := bucket.Get([]byte(groupname))
|
||||
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
|
||||
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 {
|
||||
g := bucket.Get([]byte(groupname))
|
||||
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
|
||||
err := json.Unmarshal(g, &group)
|
||||
|
@ -2116,7 +2704,7 @@ func (p *BoltProvider) removeUserFromGroupMapping(username, groupname string, bu
|
|||
users = append(users, u)
|
||||
}
|
||||
}
|
||||
group.Users = users
|
||||
group.Users = util.RemoveDuplicates(users, false)
|
||||
buf, err := json.Marshal(group)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -2372,7 +2960,7 @@ func (p *BoltProvider) getGroupsBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
|
|||
var err error
|
||||
bucket := tx.Bucket(groupsBucket)
|
||||
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
|
||||
}
|
||||
|
@ -2381,7 +2969,25 @@ func (p *BoltProvider) getFoldersBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
|
|||
var err error
|
||||
bucket := tx.Bucket(foldersBucket)
|
||||
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
|
||||
}
|
||||
|
@ -2405,7 +3011,7 @@ func getBoltDatabaseVersion(dbHandle *bolt.DB) (schemaVersion, error) {
|
|||
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 {
|
||||
bucket := tx.Bucket(dbVersionBucket)
|
||||
if bucket == nil {
|
||||
|
@ -2421,4 +3027,4 @@ func getBoltDatabaseVersion(dbHandle *bolt.DB) (schemaVersion, error) {
|
|||
return bucket.Put(dbVersionKey, buf)
|
||||
})
|
||||
return err
|
||||
}*/
|
||||
}
|
||||
|
|
|
@ -72,7 +72,7 @@ const (
|
|||
CockroachDataProviderName = "cockroachdb"
|
||||
// DumpVersion defines the version for the dump.
|
||||
// For restore/load we support the current version and the previous one
|
||||
DumpVersion = 12
|
||||
DumpVersion = 13
|
||||
|
||||
argonPwdPrefix = "$argon2id$"
|
||||
bcryptPwdPrefix = "$2a$"
|
||||
|
@ -168,6 +168,10 @@ var (
|
|||
sqlTableUsersGroupsMapping string
|
||||
sqlTableGroupsFoldersMapping string
|
||||
sqlTableSharedSessions string
|
||||
sqlTableEventsActions string
|
||||
sqlTableEventsRules string
|
||||
sqlTableRulesActionsMapping string
|
||||
sqlTableTasks string
|
||||
sqlTableSchemaVersion string
|
||||
argon2Params *argon2id.Params
|
||||
lastLoginMinDelay = 10 * time.Minute
|
||||
|
@ -189,6 +193,10 @@ func initSQLTables() {
|
|||
sqlTableUsersGroupsMapping = "users_groups_mapping"
|
||||
sqlTableGroupsFoldersMapping = "groups_folders_mapping"
|
||||
sqlTableSharedSessions = "shared_sessions"
|
||||
sqlTableEventsActions = "events_actions"
|
||||
sqlTableEventsRules = "events_rules"
|
||||
sqlTableRulesActionsMapping = "rules_actions_mapping"
|
||||
sqlTableTasks = "tasks"
|
||||
sqlTableSchemaVersion = "schema_version"
|
||||
}
|
||||
|
||||
|
@ -250,24 +258,6 @@ type ProviderStatus struct {
|
|||
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
|
||||
type Config struct {
|
||||
// Driver name, must be one of the SupportedProviders
|
||||
|
@ -417,8 +407,6 @@ type Config struct {
|
|||
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
|
||||
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
|
||||
|
@ -430,7 +418,7 @@ func (c *Config) GetShared() int {
|
|||
}
|
||||
|
||||
func (c *Config) convertName(name string) string {
|
||||
if c.NamingRules == 0 {
|
||||
if c.NamingRules <= 1 {
|
||||
return name
|
||||
}
|
||||
if c.NamingRules&2 != 0 {
|
||||
|
@ -464,31 +452,32 @@ func (c *Config) requireCustomTLSForMySQL() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (c *Config) doBackup() {
|
||||
now := time.Now()
|
||||
outputFile := filepath.Join(c.BackupsPath, fmt.Sprintf("backup_%v_%v.json", now.Weekday(), now.Hour()))
|
||||
providerLog(logger.LevelDebug, "starting auto backup to file %#v", outputFile)
|
||||
func (c *Config) doBackup() error {
|
||||
now := time.Now().UTC()
|
||||
outputFile := filepath.Join(c.BackupsPath, fmt.Sprintf("backup_%s_%d.json", now.Weekday(), now.Hour()))
|
||||
eventManagerLog(logger.LevelDebug, "starting backup to file %q", outputFile)
|
||||
err := os.MkdirAll(filepath.Dir(outputFile), 0700)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "unable to create backup dir %#v: %v", outputFile, err)
|
||||
return
|
||||
eventManagerLog(logger.LevelError, "unable to create backup dir %q: %v", outputFile, err)
|
||||
return fmt.Errorf("unable to create backup dir: %w", err)
|
||||
}
|
||||
backup, err := DumpData()
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "unable to execute backup: %v", err)
|
||||
return
|
||||
eventManagerLog(logger.LevelError, "unable to execute backup: %v", err)
|
||||
return fmt.Errorf("unable to dump backup data: %w", err)
|
||||
}
|
||||
dump, err := json.Marshal(backup)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "unable to marshal backup as JSON: %v", err)
|
||||
return
|
||||
eventManagerLog(logger.LevelError, "unable to marshal backup as JSON: %v", err)
|
||||
return fmt.Errorf("unable to marshal backup data as JSON: %w", err)
|
||||
}
|
||||
err = os.WriteFile(outputFile, dump, 0600)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "unable to save backup: %v", err)
|
||||
return
|
||||
eventManagerLog(logger.LevelError, "unable to save backup: %v", err)
|
||||
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
|
||||
|
@ -592,6 +581,8 @@ type BackupData struct {
|
|||
Admins []Admin `json:"admins"`
|
||||
APIKeys []APIKey `json:"api_keys"`
|
||||
Shares []Share `json:"shares"`
|
||||
EventActions []BaseEventAction `json:"event_actions"`
|
||||
EventRules []EventRule `json:"event_rules"`
|
||||
Version int `json:"version"`
|
||||
}
|
||||
|
||||
|
@ -704,6 +695,23 @@ type Provider interface {
|
|||
deleteSharedSession(key string) error
|
||||
getSharedSession(key string) (Session, 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
|
||||
close() error
|
||||
reloadConfig() error
|
||||
|
@ -851,13 +859,19 @@ func validateSQLTablesPrefix() error {
|
|||
sqlTableUsersGroupsMapping = config.SQLTablesPrefix + sqlTableUsersGroupsMapping
|
||||
sqlTableGroupsFoldersMapping = config.SQLTablesPrefix + sqlTableGroupsFoldersMapping
|
||||
sqlTableSharedSessions = config.SQLTablesPrefix + sqlTableSharedSessions
|
||||
sqlTableEventsActions = config.SQLTablesPrefix + sqlTableEventsActions
|
||||
sqlTableEventsRules = config.SQLTablesPrefix + sqlTableEventsRules
|
||||
sqlTableRulesActionsMapping = config.SQLTablesPrefix + sqlTableRulesActionsMapping
|
||||
sqlTableTasks = config.SQLTablesPrefix + sqlTableTasks
|
||||
sqlTableSchemaVersion = config.SQLTablesPrefix + sqlTableSchemaVersion
|
||||
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 "+
|
||||
"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,
|
||||
sqlTableShares, sqlTableDefenderHosts, sqlTableDefenderEvents, sqlTableActiveTransfers, sqlTableGroups,
|
||||
sqlTableUsersGroupsMapping, sqlTableGroupsFoldersMapping, sqlTableSharedSessions, sqlTableSchemaVersion)
|
||||
sqlTableUsersGroupsMapping, sqlTableGroupsFoldersMapping, sqlTableSharedSessions, sqlTableSchemaVersion,
|
||||
sqlTableEventsActions, sqlTableEventsRules, sqlTableRulesActionsMapping, sqlTableTasks)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -1468,6 +1482,102 @@ func APIKeyExists(keyID string) (APIKey, error) {
|
|||
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
|
||||
// and so SFTPGo is ready to be used
|
||||
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)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
var data BackupData
|
||||
groups, err := provider.dumpGroups()
|
||||
|
@ -1821,12 +1931,22 @@ func DumpData() (BackupData, error) {
|
|||
if err != nil {
|
||||
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.Groups = groups
|
||||
data.Folders = folders
|
||||
data.Admins = admins
|
||||
data.APIKeys = apiKeys
|
||||
data.Shares = shares
|
||||
data.EventActions = actions
|
||||
data.EventRules = rules
|
||||
data.Version = DumpVersion
|
||||
return data, err
|
||||
}
|
||||
|
|
1096
dataprovider/eventrule.go
Normal file
1096
dataprovider/eventrule.go
Normal file
File diff suppressed because it is too large
Load diff
460
dataprovider/eventruleutil.go
Normal file
460
dataprovider/eventruleutil.go
Normal 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...)
|
||||
}
|
|
@ -48,6 +48,14 @@ type memoryProviderHandle struct {
|
|||
shares map[string]Share
|
||||
// slice with ordered shares shareID
|
||||
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
|
||||
|
@ -78,6 +86,10 @@ func initializeMemoryProvider(basePath string) {
|
|||
apiKeysIDs: []string{},
|
||||
shares: make(map[string]Share),
|
||||
sharesIDs: []string{},
|
||||
actions: make(map[string]BaseEventAction),
|
||||
actionsNames: []string{},
|
||||
rules: make(map[string]EventRule),
|
||||
rulesNames: []string{},
|
||||
configFile: configFile,
|
||||
},
|
||||
}
|
||||
|
@ -576,14 +588,28 @@ func (p *MemoryProvider) userExistsInternal(username string) (User, error) {
|
|||
if val, ok := p.dbHandle.users[username]; ok {
|
||||
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) {
|
||||
if val, ok := p.dbHandle.groups[name]; ok {
|
||||
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 {
|
||||
|
@ -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 {
|
||||
g, err := p.groupExistsInternal(groupname)
|
||||
if err != nil {
|
||||
|
@ -1768,6 +1840,359 @@ func (p *MemoryProvider) cleanupSharedSessions(sessionType SessionType, before i
|
|||
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 {
|
||||
nextID := int64(1)
|
||||
for _, v := range p.dbHandle.users {
|
||||
|
@ -1808,6 +2233,26 @@ func (p *MemoryProvider) getNextGroupID() int64 {
|
|||
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() {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
|
@ -1856,27 +2301,35 @@ func (p *MemoryProvider) reloadConfig() error {
|
|||
}
|
||||
p.clear()
|
||||
|
||||
if err := p.restoreFolders(&dump); err != nil {
|
||||
if err := p.restoreFolders(dump); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.restoreGroups(&dump); err != nil {
|
||||
if err := p.restoreGroups(dump); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.restoreUsers(&dump); err != nil {
|
||||
if err := p.restoreUsers(dump); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.restoreAdmins(&dump); err != nil {
|
||||
if err := p.restoreAdmins(dump); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.restoreAPIKeys(&dump); err != nil {
|
||||
if err := p.restoreAPIKeys(dump); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -1884,7 +2337,51 @@ func (p *MemoryProvider) reloadConfig() error {
|
|||
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 {
|
||||
s, err := p.shareExists(share.ShareID, "")
|
||||
share := share // pin
|
||||
|
@ -1907,7 +2404,7 @@ func (p *MemoryProvider) restoreShares(dump *BackupData) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) restoreAPIKeys(dump *BackupData) error {
|
||||
func (p *MemoryProvider) restoreAPIKeys(dump BackupData) error {
|
||||
for _, apiKey := range dump.APIKeys {
|
||||
if apiKey.Key == "" {
|
||||
return fmt.Errorf("cannot restore an empty API key: %+v", apiKey)
|
||||
|
@ -1932,7 +2429,7 @@ func (p *MemoryProvider) restoreAPIKeys(dump *BackupData) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) restoreAdmins(dump *BackupData) error {
|
||||
func (p *MemoryProvider) restoreAdmins(dump BackupData) error {
|
||||
for _, admin := range dump.Admins {
|
||||
admin := admin // pin
|
||||
admin.Username = config.convertName(admin.Username)
|
||||
|
@ -1955,7 +2452,7 @@ func (p *MemoryProvider) restoreAdmins(dump *BackupData) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) restoreGroups(dump *BackupData) error {
|
||||
func (p *MemoryProvider) restoreGroups(dump BackupData) error {
|
||||
for _, group := range dump.Groups {
|
||||
group := group // pin
|
||||
group.Name = config.convertName(group.Name)
|
||||
|
@ -1979,7 +2476,7 @@ func (p *MemoryProvider) restoreGroups(dump *BackupData) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) restoreFolders(dump *BackupData) error {
|
||||
func (p *MemoryProvider) restoreFolders(dump BackupData) error {
|
||||
for _, folder := range dump.Folders {
|
||||
folder := folder // pin
|
||||
folder.Name = config.convertName(folder.Name)
|
||||
|
@ -2003,7 +2500,7 @@ func (p *MemoryProvider) restoreFolders(dump *BackupData) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) restoreUsers(dump *BackupData) error {
|
||||
func (p *MemoryProvider) restoreUsers(dump BackupData) error {
|
||||
for _, user := range dump.Users {
|
||||
user := user // pin
|
||||
user.Username = config.convertName(user.Username)
|
||||
|
|
|
@ -36,6 +36,10 @@ const (
|
|||
"DROP TABLE IF EXISTS `{{defender_hosts}}` CASCADE;" +
|
||||
"DROP TABLE IF EXISTS `{{active_transfers}}` 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;"
|
||||
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, " +
|
||||
|
@ -119,6 +123,33 @@ const (
|
|||
"CREATE INDEX `{{prefix}}shared_sessions_type_idx` ON `{{shared_sessions}}` (`type`);" +
|
||||
"CREATE INDEX `{{prefix}}shared_sessions_timestamp_idx` ON `{{shared_sessions}}` (`timestamp`);" +
|
||||
"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
|
||||
|
@ -503,6 +534,74 @@ func (p *MySQLProvider) cleanupSharedSessions(sessionType SessionType, before in
|
|||
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 {
|
||||
return p.dbHandle.Close()
|
||||
}
|
||||
|
@ -542,6 +641,8 @@ func (p *MySQLProvider) migrateDatabase() error { //nolint:dupl
|
|||
providerLog(logger.LevelError, "%v", err)
|
||||
logger.ErrorToConsole("%v", err)
|
||||
return err
|
||||
case version == 19:
|
||||
return updateMySQLDatabaseFromV19(p.dbHandle)
|
||||
default:
|
||||
if version > sqlDatabaseVersion {
|
||||
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 {
|
||||
case 20:
|
||||
return downgradeMySQLDatabaseFromV20(p.dbHandle)
|
||||
default:
|
||||
return fmt.Errorf("database version not handled: %v", dbVersion.Version)
|
||||
}
|
||||
|
@ -573,3 +676,34 @@ func (p *MySQLProvider) resetDatabase() error {
|
|||
sql := sqlReplaceAll(mysqlResetSQL)
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -35,6 +35,10 @@ DROP TABLE IF EXISTS "{{defender_events}}" CASCADE;
|
|||
DROP TABLE IF EXISTS "{{defender_hosts}}" CASCADE;
|
||||
DROP TABLE IF EXISTS "{{active_transfers}}" 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;
|
||||
`
|
||||
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_timestamp_idx" ON "{{shared_sessions}}" ("timestamp");
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
return p.dbHandle.Close()
|
||||
}
|
||||
|
@ -495,12 +597,6 @@ func (p *PGSQLProvider) initializeDatabase() error {
|
|||
logger.InfoToConsole("creating initial database schema, version 19")
|
||||
providerLog(logger.LevelInfo, "creating initial database schema, version 19")
|
||||
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)
|
||||
}
|
||||
|
@ -520,6 +616,8 @@ func (p *PGSQLProvider) migrateDatabase() error { //nolint:dupl
|
|||
providerLog(logger.LevelError, "%v", err)
|
||||
logger.ErrorToConsole("%v", err)
|
||||
return err
|
||||
case version == 19:
|
||||
return updatePgSQLDatabaseFromV19(p.dbHandle)
|
||||
default:
|
||||
if version > sqlDatabaseVersion {
|
||||
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 {
|
||||
case 20:
|
||||
return downgradePgSQLDatabaseFromV20(p.dbHandle)
|
||||
default:
|
||||
return fmt.Errorf("database version not handled: %v", dbVersion.Version)
|
||||
}
|
||||
|
@ -551,3 +651,34 @@ func (p *PGSQLProvider) resetDatabase() error {
|
|||
sql := sqlReplaceAll(pgsqlResetSQL)
|
||||
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
127
dataprovider/quota.go
Normal 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
|
||||
}
|
|
@ -14,10 +14,11 @@ import (
|
|||
|
||||
var (
|
||||
scheduler *cron.Cron
|
||||
lastCachesUpdate int64
|
||||
// used for bolt and memory providers, so we avoid iterating all users
|
||||
lastUserCacheUpdate int64
|
||||
// used for bolt and memory providers, so we avoid iterating all users/rules
|
||||
// to find recently modified ones
|
||||
lastUserUpdate int64
|
||||
lastRuleUpdate int64
|
||||
)
|
||||
|
||||
func stopScheduler() {
|
||||
|
@ -30,30 +31,22 @@ func stopScheduler() {
|
|||
func startScheduler() error {
|
||||
stopScheduler()
|
||||
|
||||
scheduler = cron.New()
|
||||
_, err := scheduler.AddFunc("@every 30s", checkDataprovider)
|
||||
scheduler = cron.New(cron.WithLocation(time.UTC))
|
||||
_, err := scheduler.AddFunc("@every 60s", checkDataprovider)
|
||||
if err != nil {
|
||||
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()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
EventManager.loadRules()
|
||||
scheduler.Start()
|
||||
return nil
|
||||
}
|
||||
|
||||
func addScheduledCacheUpdates() error {
|
||||
lastCachesUpdate = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
lastUserCacheUpdate = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
_, err := scheduler.AddFunc("@every 10m", checkCacheUpdates)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to schedule cache updates: %w", err)
|
||||
|
@ -70,9 +63,9 @@ func checkDataprovider() {
|
|||
}
|
||||
|
||||
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())
|
||||
users, err := provider.getRecentlyUpdatedUsers(lastCachesUpdate)
|
||||
users, err := provider.getRecentlyUpdatedUsers(lastUserCacheUpdate)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "unable to get recently updated users: %v", err)
|
||||
return
|
||||
|
@ -83,8 +76,9 @@ func checkCacheUpdates() {
|
|||
cachedPasswords.Remove(user.Username)
|
||||
}
|
||||
|
||||
lastCachesUpdate = checkTime
|
||||
providerLog(logger.LevelDebug, "end caches check, new update time %v", util.GetTimeFromMsecSinceEpoch(lastCachesUpdate))
|
||||
lastUserCacheUpdate = checkTime
|
||||
EventManager.loadRules()
|
||||
providerLog(logger.LevelDebug, "end caches check, new update time %v", util.GetTimeFromMsecSinceEpoch(lastUserCacheUpdate))
|
||||
}
|
||||
|
||||
func setLastUserUpdate() {
|
||||
|
@ -94,3 +88,11 @@ func setLastUserUpdate() {
|
|||
func getLastUserUpdate() int64 {
|
||||
return atomic.LoadInt64(&lastUserUpdate)
|
||||
}
|
||||
|
||||
func setLastRuleUpdate() {
|
||||
atomic.StoreInt64(&lastRuleUpdate, util.GetTimeAsMsSinceEpoch(time.Now()))
|
||||
}
|
||||
|
||||
func getLastRuleUpdate() int64 {
|
||||
return atomic.LoadInt64(&lastRuleUpdate)
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
sqlDatabaseVersion = 19
|
||||
sqlDatabaseVersion = 20
|
||||
defaultSQLQueryTimeout = 10 * 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, "{{active_transfers}}", sqlTableActiveTransfers)
|
||||
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)
|
||||
return sql
|
||||
}
|
||||
|
@ -655,17 +659,16 @@ func sqlCommonAddGroup(group *Group, dbHandle *sql.DB) error {
|
|||
if err := group.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
settings, err := json.Marshal(group.UserSettings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {
|
||||
q := getAddGroupQuery()
|
||||
|
||||
settings, err := json.Marshal(group.UserSettings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, q, group.Name, group.Description, util.GetTimeAsMsSinceEpoch(time.Now()),
|
||||
_, err := tx.ExecContext(ctx, q, group.Name, group.Description, util.GetTimeAsMsSinceEpoch(time.Now()),
|
||||
util.GetTimeAsMsSinceEpoch(time.Now()), string(settings))
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -678,17 +681,17 @@ func sqlCommonUpdateGroup(group *Group, dbHandle *sql.DB) error {
|
|||
if err := group.validate(); err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
@ -898,11 +901,7 @@ func sqlCommonAddUser(user *User, dbHandle *sql.DB) error {
|
|||
if err != nil {
|
||||
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()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -919,7 +918,12 @@ func sqlCommonAddUser(user *User, dbHandle *sql.DB) error {
|
|||
if err != nil {
|
||||
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.DownloadBandwidth, user.Status, user.ExpirationDate, string(filters), string(fsConfig), user.AdditionalInfo,
|
||||
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 {
|
||||
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()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -969,7 +969,12 @@ func sqlCommonUpdateUser(user *User, dbHandle *sql.DB) error {
|
|||
if err != nil {
|
||||
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.ExpirationDate, string(filters), string(fsConfig), user.AdditionalInfo, user.Description, user.Email,
|
||||
util.GetTimeAsMsSinceEpoch(time.Now()), user.UploadDataTransfer, user.DownloadDataTransfer, user.TotalDataTransfer,
|
||||
|
@ -1611,6 +1616,55 @@ func getAdminFromDbRow(row sqlScanner) (Admin, error) {
|
|||
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) {
|
||||
var group Group
|
||||
var userSettings, description sql.NullString
|
||||
|
@ -2062,7 +2116,6 @@ func getUsersWithVirtualFolders(ctx context.Context, users []User, dbHandle sqlQ
|
|||
return users, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
usersVirtualFolders := make(map[int64][]vfs.VirtualFolder)
|
||||
q := getRelatedFoldersForUsersQuery(users)
|
||||
rows, err := dbHandle.QueryContext(ctx, q)
|
||||
|
@ -2124,7 +2177,6 @@ func getUsersWithGroups(ctx context.Context, users []User, dbHandle sqlQuerier)
|
|||
if len(users) == 0 {
|
||||
return users, nil
|
||||
}
|
||||
var err error
|
||||
usersGroups := make(map[int64][]sdk.GroupMapping)
|
||||
q := getRelatedGroupsForUsersQuery(users)
|
||||
rows, err := dbHandle.QueryContext(ctx, q)
|
||||
|
@ -2182,8 +2234,6 @@ func getGroupsWithVirtualFolders(ctx context.Context, groups []Group, dbHandle s
|
|||
if len(groups) == 0 {
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
q := getRelatedFoldersForGroupsQuery(groups)
|
||||
rows, err := dbHandle.QueryContext(ctx, q)
|
||||
if err != nil {
|
||||
|
@ -2235,8 +2285,6 @@ func getGroupsWithUsers(ctx context.Context, groups []Group, dbHandle sqlQuerier
|
|||
if len(groups) == 0 {
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
q := getRelatedUsersForGroupsQuery(groups)
|
||||
rows, err := dbHandle.QueryContext(ctx, q)
|
||||
if err != nil {
|
||||
|
@ -2272,7 +2320,6 @@ func getVirtualFoldersWithGroups(folders []vfs.BaseVirtualFolder, dbHandle sqlQu
|
|||
if len(folders) == 0 {
|
||||
return folders, nil
|
||||
}
|
||||
var err error
|
||||
vFoldersGroups := make(map[int64][]string)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
@ -2310,7 +2357,6 @@ func getVirtualFoldersWithUsers(folders []vfs.BaseVirtualFolder, dbHandle sqlQue
|
|||
if len(folders) == 0 {
|
||||
return folders, nil
|
||||
}
|
||||
var err error
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
|
@ -2509,6 +2555,492 @@ func sqlCommonCleanupSessions(sessionType SessionType, before int64, dbHandle *s
|
|||
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) {
|
||||
var result schemaVersion
|
||||
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")
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
// 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 "{{active_transfers}}";
|
||||
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}}";
|
||||
`
|
||||
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_timestamp_idx" ON "{{shared_sessions}}" ("timestamp");
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
return p.dbHandle.Close()
|
||||
}
|
||||
|
@ -488,6 +588,8 @@ func (p *SQLiteProvider) migrateDatabase() error { //nolint:dupl
|
|||
providerLog(logger.LevelError, "%v", err)
|
||||
logger.ErrorToConsole("%v", err)
|
||||
return err
|
||||
case version == 19:
|
||||
return updateSQLiteDatabaseFromV19(p.dbHandle)
|
||||
default:
|
||||
if version > sqlDatabaseVersion {
|
||||
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 {
|
||||
case 20:
|
||||
return downgradeSQLiteDatabaseFromV20(p.dbHandle)
|
||||
default:
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
|
|
@ -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," +
|
||||
"s.expires_at,s.password,s.max_tokens,s.used_tokens,s.allow_from"
|
||||
selectGroupFields = "id,name,description,created_at,updated_at,user_settings"
|
||||
selectEventActionFields = "id,name,description,type,options"
|
||||
selectMinimalFields = "id,name"
|
||||
)
|
||||
|
||||
func getSQLPlaceholders() []string {
|
||||
|
@ -33,12 +35,20 @@ func getSQLPlaceholders() []string {
|
|||
return placeholders
|
||||
}
|
||||
|
||||
func getSQLTableGroups() string {
|
||||
func getSQLQuotedName(name string) string {
|
||||
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 {
|
||||
|
@ -147,18 +157,19 @@ func getDefenderEventsCleanupQuery() 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 {
|
||||
var fieldSelection string
|
||||
if minimal {
|
||||
fieldSelection = "id,name"
|
||||
fieldSelection = selectMinimalFields
|
||||
} else {
|
||||
fieldSelection = selectGroupFields
|
||||
}
|
||||
return fmt.Sprintf(`SELECT %s FROM %s ORDER BY name %s LIMIT %s OFFSET %s`, fieldSelection, getSQLTableGroups(),
|
||||
order, sqlPlaceholders[0], sqlPlaceholders[1])
|
||||
return fmt.Sprintf(`SELECT %s FROM %s ORDER BY name %s LIMIT %s OFFSET %s`, fieldSelection,
|
||||
getSQLQuotedName(sqlTableGroups), order, sqlPlaceholders[0], sqlPlaceholders[1])
|
||||
}
|
||||
|
||||
func getGroupsWithNamesQuery(numArgs int) string {
|
||||
|
@ -176,7 +187,7 @@ func getGroupsWithNamesQuery(numArgs int) string {
|
|||
} else {
|
||||
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 {
|
||||
|
@ -195,27 +206,27 @@ func getUsersInGroupsQuery(numArgs int) string {
|
|||
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)))`,
|
||||
sqlTableUsers, sqlTableUsersGroupsMapping, getSQLTableGroups(), sb.String())
|
||||
sqlTableUsers, sqlTableUsersGroupsMapping, getSQLQuotedName(sqlTableGroups), sb.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 {
|
||||
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])
|
||||
}
|
||||
|
||||
func getUpdateGroupQuery() string {
|
||||
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])
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -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,
|
||||
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,
|
||||
used_upload_data_transfer,used_download_data_transfer)
|
||||
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)`,
|
||||
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,0)`,
|
||||
sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4],
|
||||
sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7], sqlPlaceholders[8], sqlPlaceholders[9],
|
||||
sqlPlaceholders[10], sqlPlaceholders[11], sqlPlaceholders[12], sqlPlaceholders[13], sqlPlaceholders[14],
|
||||
|
@ -527,19 +538,20 @@ func getClearUserGroupMappingQuery() string {
|
|||
func getAddUserGroupMappingQuery() string {
|
||||
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)`,
|
||||
sqlTableUsersGroupsMapping, sqlTableUsers, sqlPlaceholders[0], getSQLTableGroups(), sqlPlaceholders[1], sqlPlaceholders[2])
|
||||
sqlTableUsersGroupsMapping, sqlTableUsers, sqlPlaceholders[0], getSQLQuotedName(sqlTableGroups),
|
||||
sqlPlaceholders[1], sqlPlaceholders[2])
|
||||
}
|
||||
|
||||
func getClearGroupFolderMappingQuery() string {
|
||||
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 {
|
||||
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))`,
|
||||
sqlTableGroupsFoldersMapping, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlTableFolders,
|
||||
sqlPlaceholders[3], getSQLTableGroups(), sqlPlaceholders[4])
|
||||
sqlPlaceholders[3], getSQLQuotedName(sqlTableGroups), sqlPlaceholders[4])
|
||||
}
|
||||
|
||||
func getClearUserFolderMappingQuery() string {
|
||||
|
@ -557,7 +569,7 @@ func getAddUserFolderMappingQuery() string {
|
|||
func getFoldersQuery(order string, minimal bool) string {
|
||||
var fieldSelection string
|
||||
if minimal {
|
||||
fieldSelection = "id,name"
|
||||
fieldSelection = selectMinimalFields
|
||||
} else {
|
||||
fieldSelection = selectFolderFields
|
||||
}
|
||||
|
@ -593,7 +605,7 @@ func getRelatedGroupsForUsersQuery(users []User) string {
|
|||
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
|
||||
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 {
|
||||
|
@ -645,7 +657,8 @@ func getRelatedGroupsForFoldersQuery(folders []vfs.BaseVirtualFolder) string {
|
|||
sb.WriteString(")")
|
||||
}
|
||||
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 {
|
||||
|
@ -711,6 +724,156 @@ func getCleanupActiveTransfersQuery() string {
|
|||
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 {
|
||||
return fmt.Sprintf("SELECT version from %s LIMIT 1", sqlTableSchemaVersion)
|
||||
}
|
||||
|
|
|
@ -1524,10 +1524,11 @@ func (u *User) applyGroupSettings(groupsMapping map[string]Group) {
|
|||
if u.groupSettingsApplied {
|
||||
return
|
||||
}
|
||||
replacer := u.getGroupPlacehodersReplacer()
|
||||
for _, g := range u.Groups {
|
||||
if g.Type == sdk.GroupTypePrimary {
|
||||
if group, ok := groupsMapping[g.Name]; ok {
|
||||
u.mergeWithPrimaryGroup(group)
|
||||
u.mergeWithPrimaryGroup(group, replacer)
|
||||
} else {
|
||||
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 {
|
||||
if g.Type == sdk.GroupTypeSecondary {
|
||||
if group, ok := groupsMapping[g.Name]; ok {
|
||||
u.mergeAdditiveProperties(group, sdk.GroupTypeSecondary)
|
||||
u.mergeAdditiveProperties(group, sdk.GroupTypeSecondary, replacer)
|
||||
} else {
|
||||
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 {
|
||||
return fmt.Errorf("unable to get groups: %w", err)
|
||||
}
|
||||
replacer := u.getGroupPlacehodersReplacer()
|
||||
// make sure to always merge with the primary group first
|
||||
for idx, g := range groups {
|
||||
if g.Name == primaryGroupName {
|
||||
u.mergeWithPrimaryGroup(g)
|
||||
u.mergeWithPrimaryGroup(g, replacer)
|
||||
lastIdx := len(groups) - 1
|
||||
groups[idx] = groups[lastIdx]
|
||||
groups = groups[:lastIdx]
|
||||
|
@ -1577,42 +1579,46 @@ func (u *User) LoadAndApplyGroupSettings() error {
|
|||
}
|
||||
}
|
||||
for _, g := range groups {
|
||||
u.mergeAdditiveProperties(g, sdk.GroupTypeSecondary)
|
||||
u.mergeAdditiveProperties(g, sdk.GroupTypeSecondary, replacer)
|
||||
}
|
||||
u.removeDuplicatesAfterGroupMerge()
|
||||
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 == "" {
|
||||
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 {
|
||||
case sdk.S3FilesystemProvider:
|
||||
fsConfig.S3Config.KeyPrefix = u.replacePlaceholder(fsConfig.S3Config.KeyPrefix)
|
||||
fsConfig.S3Config.KeyPrefix = u.replacePlaceholder(fsConfig.S3Config.KeyPrefix, replacer)
|
||||
case sdk.GCSFilesystemProvider:
|
||||
fsConfig.GCSConfig.KeyPrefix = u.replacePlaceholder(fsConfig.GCSConfig.KeyPrefix)
|
||||
fsConfig.GCSConfig.KeyPrefix = u.replacePlaceholder(fsConfig.GCSConfig.KeyPrefix, replacer)
|
||||
case sdk.AzureBlobFilesystemProvider:
|
||||
fsConfig.AzBlobConfig.KeyPrefix = u.replacePlaceholder(fsConfig.AzBlobConfig.KeyPrefix)
|
||||
fsConfig.AzBlobConfig.KeyPrefix = u.replacePlaceholder(fsConfig.AzBlobConfig.KeyPrefix, replacer)
|
||||
case sdk.SFTPFilesystemProvider:
|
||||
fsConfig.SFTPConfig.Username = u.replacePlaceholder(fsConfig.SFTPConfig.Username)
|
||||
fsConfig.SFTPConfig.Prefix = u.replacePlaceholder(fsConfig.SFTPConfig.Prefix)
|
||||
fsConfig.SFTPConfig.Username = u.replacePlaceholder(fsConfig.SFTPConfig.Username, replacer)
|
||||
fsConfig.SFTPConfig.Prefix = u.replacePlaceholder(fsConfig.SFTPConfig.Prefix, replacer)
|
||||
case sdk.HTTPFilesystemProvider:
|
||||
fsConfig.HTTPConfig.Username = u.replacePlaceholder(fsConfig.HTTPConfig.Username)
|
||||
fsConfig.HTTPConfig.Username = u.replacePlaceholder(fsConfig.HTTPConfig.Username, replacer)
|
||||
}
|
||||
return fsConfig
|
||||
}
|
||||
|
||||
func (u *User) mergeWithPrimaryGroup(group Group) {
|
||||
func (u *User) mergeWithPrimaryGroup(group Group, replacer *strings.Replacer) {
|
||||
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 {
|
||||
u.FsConfig = u.replaceFsConfigPlaceholders(group.UserSettings.FsConfig)
|
||||
u.FsConfig = u.replaceFsConfigPlaceholders(group.UserSettings.FsConfig, replacer)
|
||||
}
|
||||
if u.MaxSessions == 0 {
|
||||
u.MaxSessions = group.UserSettings.MaxSessions
|
||||
|
@ -1634,11 +1640,11 @@ func (u *User) mergeWithPrimaryGroup(group Group) {
|
|||
u.DownloadDataTransfer = group.UserSettings.DownloadDataTransfer
|
||||
u.TotalDataTransfer = group.UserSettings.TotalDataTransfer
|
||||
}
|
||||
u.mergePrimaryGroupFilters(group.UserSettings.Filters)
|
||||
u.mergeAdditiveProperties(group, sdk.GroupTypePrimary)
|
||||
u.mergePrimaryGroupFilters(group.UserSettings.Filters, replacer)
|
||||
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 {
|
||||
u.Filters.MaxUploadFileSize = filters.MaxUploadFileSize
|
||||
}
|
||||
|
@ -1664,14 +1670,14 @@ func (u *User) mergePrimaryGroupFilters(filters sdk.BaseUserFilters) {
|
|||
u.Filters.ExternalAuthCacheTime = filters.ExternalAuthCacheTime
|
||||
}
|
||||
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) {
|
||||
u.mergeVirtualFolders(group, groupType)
|
||||
u.mergePermissions(group, groupType)
|
||||
u.mergeFilePatterns(group, groupType)
|
||||
func (u *User) mergeAdditiveProperties(group Group, groupType int, replacer *strings.Replacer) {
|
||||
u.mergeVirtualFolders(group, groupType, replacer)
|
||||
u.mergePermissions(group, groupType, replacer)
|
||||
u.mergeFilePatterns(group, groupType, replacer)
|
||||
u.Filters.BandwidthLimits = append(u.Filters.BandwidthLimits, group.UserSettings.Filters.BandwidthLimits...)
|
||||
u.Filters.DataTransferLimits = append(u.Filters.DataTransferLimits, group.UserSettings.Filters.DataTransferLimits...)
|
||||
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...)
|
||||
}
|
||||
|
||||
func (u *User) mergeVirtualFolders(group Group, groupType int) {
|
||||
func (u *User) mergeVirtualFolders(group Group, groupType int, replacer *strings.Replacer) {
|
||||
if len(group.VirtualFolders) > 0 {
|
||||
folderPaths := make(map[string]bool)
|
||||
for _, folder := range u.VirtualFolders {
|
||||
|
@ -1692,17 +1698,17 @@ func (u *User) mergeVirtualFolders(group Group, groupType int) {
|
|||
if folder.VirtualPath == "/" && groupType != sdk.GroupTypePrimary {
|
||||
continue
|
||||
}
|
||||
folder.VirtualPath = u.replacePlaceholder(folder.VirtualPath)
|
||||
folder.VirtualPath = u.replacePlaceholder(folder.VirtualPath, replacer)
|
||||
if _, ok := folderPaths[folder.VirtualPath]; !ok {
|
||||
folder.MappedPath = u.replacePlaceholder(folder.MappedPath)
|
||||
folder.FsConfig = u.replaceFsConfigPlaceholders(folder.FsConfig)
|
||||
folder.MappedPath = u.replacePlaceholder(folder.MappedPath, replacer)
|
||||
folder.FsConfig = u.replaceFsConfigPlaceholders(folder.FsConfig, replacer)
|
||||
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 {
|
||||
if k == "/" {
|
||||
if groupType == sdk.GroupTypePrimary {
|
||||
|
@ -1711,14 +1717,14 @@ func (u *User) mergePermissions(group Group, groupType int) {
|
|||
continue
|
||||
}
|
||||
}
|
||||
k = u.replacePlaceholder(k)
|
||||
k = u.replacePlaceholder(k, replacer)
|
||||
if _, ok := u.Permissions[k]; !ok {
|
||||
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 {
|
||||
patternPaths := make(map[string]bool)
|
||||
for _, pattern := range u.Filters.FilePatterns {
|
||||
|
@ -1728,7 +1734,7 @@ func (u *User) mergeFilePatterns(group Group, groupType int) {
|
|||
if pattern.Path == "/" && groupType != sdk.GroupTypePrimary {
|
||||
continue
|
||||
}
|
||||
pattern.Path = u.replacePlaceholder(pattern.Path)
|
||||
pattern.Path = u.replacePlaceholder(pattern.Path, replacer)
|
||||
if _, ok := patternPaths[pattern.Path]; !ok {
|
||||
u.Filters.FilePatterns = append(u.Filters.FilePatterns, pattern)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
- `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`
|
||||
- `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
|
||||
- `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
|
||||
|
@ -85,8 +85,12 @@ The `actions` struct inside the `data_provider` configuration section allows you
|
|||
The supported object types are:
|
||||
|
||||
- `user`
|
||||
- `group`
|
||||
- `admin`
|
||||
- `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.
|
||||
|
||||
|
|
47
docs/eventmanager.md
Normal file
47
docs/eventmanager.md
Normal 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 rule’s 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.
|
|
@ -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.
|
||||
- `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`.
|
||||
- `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.
|
||||
- `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
|
||||
- `bindings`, list of structs. Each struct has the following fields:
|
||||
- `port`, integer. The port used for serving HTTP requests. Default: 8080.
|
||||
|
|
|
@ -77,154 +77,154 @@ CzgWkxiz7XE4lgUwX44FCXZM3+JeUbI=
|
|||
-----END EC PRIVATE KEY-----`
|
||||
caCRT = `-----BEGIN CERTIFICATE-----
|
||||
MIIE5jCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0
|
||||
QXV0aDAeFw0yMTAxMDIyMTIwNTVaFw0yMjA3MDIyMTMwNTJaMBMxETAPBgNVBAMT
|
||||
CENlcnRBdXRoMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4Tiho5xW
|
||||
AC15JRkMwfp3/TJwI2As7MY5dele5cmdr5bHAE+sRKqC+Ti88OJWCV5saoyax/1S
|
||||
CjxJlQMZMl169P1QYJskKjdG2sdv6RLWLMgwSNRRjxp/Bw9dHdiEb9MjLgu28Jro
|
||||
9peQkHcRHeMf5hM9WvlIJGrdzbC4hUehmqggcqgARainBkYjf0SwuWxHeu4nMqkp
|
||||
Ak5tcSTLCjHfEFHZ9Te0TIPG5YkWocQKyeLgu4lvuU+DD2W2lym+YVUtRMGs1Env
|
||||
k7p+N0DcGU26qfzZ2sF5ZXkqm7dBsGQB9pIxwc2Q8T1dCIyP9OQCKVILdc5aVFf1
|
||||
cryQFHYzYNNZXFlIBims5VV5Mgfp8ESHQSue+v6n6ykecLEyKt1F1Y/MWY/nWUSI
|
||||
8zdq83jdBAZVjo9MSthxVn57/06s/hQca65IpcTZV2gX0a+eRlAVqaRbAhL3LaZe
|
||||
bYsW3WHKoUOftwemuep3nL51TzlXZVL7Oz/ClGaEOsnGG9KFO6jh+W768qC0zLQI
|
||||
CdE7v2Zex98sZteHCg9fGJHIaYoF0aJG5P3WI5oZf2fy7UIYN9ADLFZiorCXAZEh
|
||||
CSU6mDoRViZ4RGR9GZxbDZ9KYn7O8M/KCR72bkQg73TlMsk1zSXEw0MKLUjtsw6c
|
||||
rZ0Jt8t3sRatHO3JrYHALMt9vZfyNCZp0IsCAwEAAaNFMEMwDgYDVR0PAQH/BAQD
|
||||
AgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFO1yCNAGr/zQTJIi8lw3
|
||||
w5OiuBvMMA0GCSqGSIb3DQEBCwUAA4ICAQA6gCNuM7r8mnx674dm31GxBjQy5ZwB
|
||||
7CxDzYEvL/oiZ3Tv3HlPfN2LAAsJUfGnghh9DOytenL2CTZWjl/emP5eijzmlP+9
|
||||
zva5I6CIMCf/eDDVsRdO244t0o4uG7+At0IgSDM3bpVaVb4RHZNjEziYChsEYY8d
|
||||
HK6iwuRSvFniV6yhR/Vj1Ymi9yZ5xclqseLXiQnUB0PkfIk23+7s42cXB16653fH
|
||||
O/FsPyKBLiKJArizLYQc12aP3QOrYoYD9+fAzIIzew7A5C0aanZCGzkuFpO6TRlD
|
||||
Tb7ry9Gf0DfPpCgxraH8tOcmnqp/ka3hjqo/SRnnTk0IFrmmLdarJvjD46rKwBo4
|
||||
MjyAIR1mQ5j8GTlSFBmSgETOQ/EYvO3FPLmra1Fh7L+DvaVzTpqI9fG3TuyyY+Ri
|
||||
Fby4ycTOGSZOe5Fh8lqkX5Y47mCUJ3zHzOA1vUJy2eTlMRGpu47Eb1++Vm6EzPUP
|
||||
2EF5aD+zwcssh+atZvQbwxpgVqVcyLt91RSkKkmZQslh0rnlTb68yxvUnD3zw7So
|
||||
o6TAf9UvwVMEvdLT9NnFd6hwi2jcNte/h538GJwXeBb8EkfpqLKpTKyicnOdkamZ
|
||||
7E9zY8SHNRYMwB9coQ/W8NvufbCgkvOoLyMXk5edbXofXl3PhNGOlraWbghBnzf5
|
||||
r3rwjFsQOoZotA==
|
||||
QXV0aDAeFw0yMjA3MDQxNTQzMTFaFw0yNDAxMDQxNTUzMDhaMBMxETAPBgNVBAMT
|
||||
CENlcnRBdXRoMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4eyDJkmW
|
||||
D4OVYo7ddgiZkd6QQdPyLcsa31Wc9jdR2/peEabyNT8jSWteS6ouY84GRlnhfFeZ
|
||||
mpXgbaUJu/Z8Y/8riPxwL8XF4vCScQDMywpQnVUd6E9x2/+/uaD4p/BBswgKqKPe
|
||||
uDcHZn7MkD4QlquUhMElDrBUi1Dv/AVHnQ6iP4vd5Jlv0F+40jdq/8Wa7yhW7Pu5
|
||||
iNvPwCk8HjENBKVur/re+Acif8A2TlbCsuOnVduSQNmnWH+iZmB9upyBZtUszGS0
|
||||
JhUwtSnwUX/JapF70Pwte/PV3RK8cJ5FjuAPNeTyJvSuMTELFSAyCeiNynFGgyhW
|
||||
cqbEiPu6BURLculyVkmh4dOrhTrYZv/n3UJAhyxkdYrbh3INHmTa4izvclcuwoEo
|
||||
lFlJp3l77D0lIi+pbtcBV6ys7reyuxUAkBNwnpt2pWfCQoi4QYKcNbHm47c2phOb
|
||||
QSojQ8SsNU5bnlY2MDzkKo5DPav/i4d0HpndphUpx4f8hA0KylLevDRkMz9TAH7H
|
||||
uDssn0CxFOGHiveEAGGbn+doHjNWM339x/cdLbK0vuieDKby8YYcBY1JML57Dl9f
|
||||
rs52ySnDZbMqOb9zF66mQpC2FZoAj713xSkDSnSCUekrqgck1EA1ifxAviHt+p26
|
||||
JwaEDL7Lk01EEdYN4csSd1fezbCqTrG8ffUCAwEAAaNFMEMwDgYDVR0PAQH/BAQD
|
||||
AgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFPirPBPO01zUuf7xC+ds
|
||||
bOOY5QvAMA0GCSqGSIb3DQEBCwUAA4ICAQBUYa+ydfTPKjTN4lXyEZgchZQ+juny
|
||||
aMy1xosLz6Evj0us2Bwczmy6X2Zvaw/KteFlgKaU1Ex2UkU7FfAlaH0HtwTLFMVM
|
||||
p9nB7ZzStvg0n8zFM29SEkOFwZ9FRonxx4sY3FdvI4QvAWyDyqgOl8+Eedg0kC4+
|
||||
M7hxarTFmZZ7POZl8Hio592yx3asMmSCcmb7oUCKVI98qsf9fuL+LIZSpn4fE7av
|
||||
AiNBcOqCZ10CRnl4VSgAW2LH4oqROYdUv+me1u1YRwh7fCF/R7VjOLuaDzv0mp/g
|
||||
hzG9U+Yso3WV4b28MsctwUmGTK8Zc5QaANKgmI3ulkta37wN5KjrUuescHC7MqZg
|
||||
vN9n60801be1EoUL83KUx57Bix95YZR02Zge0gYdYTb+E2bwaZ4GMlf7cs6qmC6A
|
||||
ZPLR7Tffw2J4dPTcfEx3rPZ91s3MkAdPzYYGdGlbKp8RCFnezZ7rw2z57rnT0zDr
|
||||
LuL3Q6ADBfothoos/EBIC5ekXb9czp8gig+nJXLC6jlqcQpCLrV88oS3+8zACmx1
|
||||
d6tje9uuAqPgiQGddKZj4b4BlHmAMXq0PufQsZVoyzboTewZiLVCtTR9/iF7Cepg
|
||||
6EVv57p61pFhPu8lNRAi0aH/po9yt+7435FGpn2kan6k9aDIVdaqeuxxITwsqJ4R
|
||||
WwSa13hh6yjoDQ==
|
||||
-----END CERTIFICATE-----`
|
||||
caCRL = `-----BEGIN X509 CRL-----
|
||||
MIICpzCBkAIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0QXV0aBcN
|
||||
MjEwMTAyMjEzNDA1WhcNMjMwMTAyMjEzNDA1WjAkMCICEQC+l04DbHWMyC3fG09k
|
||||
VXf+Fw0yMTAxMDIyMTM0MDVaoCMwITAfBgNVHSMEGDAWgBTtcgjQBq/80EySIvJc
|
||||
N8OTorgbzDANBgkqhkiG9w0BAQsFAAOCAgEAEJ7z+uNc8sqtxlOhSdTGDzX/xput
|
||||
E857kFQkSlMnU2whQ8c+XpYrBLA5vIZJNSSwohTpM4+zVBX/bJpmu3wqqaArRO9/
|
||||
YcW5mQk9Anvb4WjQW1cHmtNapMTzoC9AiYt/OWPfy+P6JCgCr4Hy6LgQyIRL6bM9
|
||||
VYTalolOm1qa4Y5cIeT7iHq/91mfaqo8/6MYRjLl8DOTROpmw8OS9bCXkzGKdCat
|
||||
AbAzwkQUSauyoCQ10rpX+Y64w9ng3g4Dr20aCqPf5osaqplEJ2HTK8ljDTidlslv
|
||||
9anQj8ax3Su89vI8+hK+YbfVQwrThabgdSjQsn+veyx8GlP8WwHLAQ379KjZjWg+
|
||||
OlOSwBeU1vTdP0QcB8X5C2gVujAyuQekbaV86xzIBOj7vZdfHZ6ee30TZ2FKiMyg
|
||||
7/N2OqW0w77ChsjB4MSHJCfuTgIeg62GzuZXLM+Q2Z9LBdtm4Byg+sm/P52adOEg
|
||||
gVb2Zf4KSvsAmA0PIBlu449/QXUFcMxzLFy7mwTeZj2B4Ln0Hm0szV9f9R8MwMtB
|
||||
SyLYxVH+mgqaR6Jkk22Q/yYyLPaELfafX5gp/AIXG8n0zxfVaTvK3auSgb1Q6ZLS
|
||||
5QH9dSIsmZHlPq7GoSXmKpMdjUL8eaky/IMteioyXgsBiATzl5L2dsw6MTX3MDF0
|
||||
QbDK+MzhmbKfDxs=
|
||||
MjIwNzA0MTU1MzU4WhcNMjQwNzAzMTU1MzU4WjAkMCICEQDZo5Q3lhxFuDUsxGNm
|
||||
794YFw0yMjA3MDQxNTUzNThaoCMwITAfBgNVHSMEGDAWgBT4qzwTztNc1Ln+8Qvn
|
||||
bGzjmOULwDANBgkqhkiG9w0BAQsFAAOCAgEA1lK6g8qmhyY6myx8342dDuaauY03
|
||||
0iojkxpasuYcytK6XRm96YqjZK9EETxsHHViVU0vCXES60D6wJ9gw4fTWn3WxEdx
|
||||
nIwbGyjUGHh2y+R3uQsfvwxsdYvDsTLAnOLwOo68dAHWmMDZRmgTuGNoYFxVQRGR
|
||||
Cn90ZR7LPLpCScclWM8FE/W1B90x3ZE8EhJiCI/WyyTh3EgshmB7A5GoDrFZfmvR
|
||||
dzoTKO+F9p2XjtmgfiBE3czWQysfATmbutZUbG/ZRb89u+ZEUyPoC94mg8fhNWoX
|
||||
1d5G9QAkZFHp957/5QHLq9OHNfnWXoohhebjF4VWqZH7w+RtLc8t0PIog2lX4t1o
|
||||
5N/xFk9akvuoyNGg/fYuJBmN162Q0MdeYfYKDGWdXxf6fpHxVr5v2JrIx6gOwubb
|
||||
cIKP22ZBv/PYOeFsAZ755lTl4OTFUjU5ZJEPD6pUc1daaIqfxsxu8gDZP92FZjsB
|
||||
zaalMbh30n2OhagSMBzSLg5rE6WmBzlQX0ZN8YrW4l2Vq6twnnFHY+UyblRZS+d4
|
||||
oHBaoOaxPEkLxNZ8ulzJS4B6c4D1CXOaBEf++snVzRRUOEdX3x7TvkkrLvIsm06R
|
||||
ux0L1zJb9LbZ/1rhuv70z/kIlD55sqYuRqu3RpgTgZuTERU//rYIqWd03Y5Qon8i
|
||||
VoC6Yp9DPldQJrk=
|
||||
-----END X509 CRL-----`
|
||||
client1Crt = `-----BEGIN CERTIFICATE-----
|
||||
MIIEITCCAgmgAwIBAgIRAIppZHoj1hM80D7WzTEKLuAwDQYJKoZIhvcNAQELBQAw
|
||||
EzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjEwMTAyMjEyMzEwWhcNMjIwNzAyMjEz
|
||||
MDUxWjASMRAwDgYDVQQDEwdjbGllbnQxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
|
||||
MIIBCgKCAQEAoKbYY9MdF2kF/nhBESIiZTdVYtA8XL9xrIZyDj9EnCiTxHiVbJtH
|
||||
XVwszqSl5TRrotPmnmAQcX3r8OCk+z+RQZ0QQj257P3kG6q4rNnOcWCS5xEd20jP
|
||||
yhQ3m+hMGfZsotNTQze1ochuQgLUN6IPyPxZkH22ia3jX4iu1eo/QxeLYHj1UHw4
|
||||
3Cii9yE+j5kPUC21xmnrGKdUrB55NYLXHx6yTIqYR5znSOVB8oJi18/hwdZmH859
|
||||
DHhm0Hx1HrS+jbjI3+CMorZJ3WUyNf+CkiVLD3xYutPbxzEpwiqkG/XYzLH0habT
|
||||
cDcILo18n+o3jvem2KWBrDhyairjIDscwQIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC
|
||||
A7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBSJ5GIv
|
||||
zIrE4ZSQt2+CGblKTDswizAfBgNVHSMEGDAWgBTtcgjQBq/80EySIvJcN8OTorgb
|
||||
zDANBgkqhkiG9w0BAQsFAAOCAgEALh4f5GhvNYNou0Ab04iQBbLEdOu2RlbK1B5n
|
||||
K9P/umYenBHMY/z6HT3+6tpcHsDuqE8UVdq3f3Gh4S2Gu9m8PRitT+cJ3gdo9Plm
|
||||
3rD4ufn/s6rGg3ppydXcedm17492tbccUDWOBZw3IO/ASVq13WPgT0/Kev7cPq0k
|
||||
sSdSNhVeXqx8Myc2/d+8GYyzbul2Kpfa7h9i24sK49E9ftnSmsIvngONo08eT1T0
|
||||
3wAOyK2981LIsHaAWcneShKFLDB6LeXIT9oitOYhiykhFlBZ4M1GNlSNfhQ8IIQP
|
||||
xbqMNXCLkW4/BtLhGEEcg0QVso6Kudl9rzgTfQknrdF7pHp6rS46wYUjoSyIY6dl
|
||||
oLmnoAVJX36J3QPWelePI9e07X2wrTfiZWewwgw3KNRWjd6/zfPLe7GoqXnK1S2z
|
||||
PT8qMfCaTwKTtUkzXuTFvQ8bAo2My/mS8FOcpkt2oQWeOsADHAUX7fz5BCoa2DL3
|
||||
k/7Mh4gVT+JYZEoTwCFuYHgMWFWe98naqHi9lB4yR981p1QgXgxO7qBeipagKY1F
|
||||
LlH1iwXUqZ3MZnkNA+4e1Fglsw3sa/rC+L98HnznJ/YbTfQbCP6aQ1qcOymrjMud
|
||||
7MrFwqZjtd/SK4Qx1VpK6jGEAtPgWBTUS3p9ayg6lqjMBjsmySWfvRsDQbq6P5Ct
|
||||
O/e3EH8=
|
||||
MIIEITCCAgmgAwIBAgIRAJla/m/UkZMifNwG+DxFr2MwDQYJKoZIhvcNAQELBQAw
|
||||
EzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjIwNzA0MTU0MzM3WhcNMjQwMTA0MTU1
|
||||
MzA3WjASMRAwDgYDVQQDEwdjbGllbnQxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
|
||||
MIIBCgKCAQEA8xM5v+2QfdzfwnNT5cl+6oEy2fZoI2YG6L6c25rG0pr+yl1IHKdM
|
||||
Zcvn93uat7hlbzxeOLfJRM7+QK1lLaxuppq9p+gT+1x9eG3E4X7e0pdbjrpJGbvN
|
||||
ji0hwDBLDWD8mHNq/SCk9FKtGnfZqrNB5BLw2uIKjJzVGXVlsjN6geBDm2hVjTSm
|
||||
zMr39CfLUdtvMaZhpIPJzbH+sNfp1zKavFIpmwCd77p/z0QAiQ9NaIvzv4PZDDEE
|
||||
MUHzmVAU6bUjD8GToXaMbRiz694SU8aAwvvcdjGexdbHnfSAfLOl2wTPPxvePncR
|
||||
aa656ZeZWxY9pRCItP+v43nm7d4sAyRD4QIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC
|
||||
A7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBQbwDqF
|
||||
aja3ifZHm6mtSeTK9IHc+zAfBgNVHSMEGDAWgBT4qzwTztNc1Ln+8QvnbGzjmOUL
|
||||
wDANBgkqhkiG9w0BAQsFAAOCAgEAprE/zV6u8UIH8g4Jb73wtUD/eIL3iBJ7mNYa
|
||||
lqwCyJrWH7/F9fcovJnF9WO1QPTeHxhoD9rlQK70GitUAeboYw611yNWDS4tDlaL
|
||||
sjpJKykUxBgBR7QSLZCrPtQ3fP2WvlZzLGqB28rASTLphShqTuGp4gJaxGHfbCU7
|
||||
mlV9QYi+InQxOICJJPebXUOwx5wYkFQWJ9qE1AK3QrWPi8QYFznJvHgkNAaMBEmI
|
||||
jAlggOzpveVvy8f4z3QG9o29LIwp7JvtJQs7QXL80FZK98/8US/3gONwTrBz2Imx
|
||||
28ywvwCq7fpMyPgxX4sXtxphCNim+vuHcqDn2CvLS9p/6L6zzqbFNxpmMkJDLrOc
|
||||
YqtHE4TLWIaXpb5JNrYJgNCZyJuYDICVTbivtMacHpSwYtXQ4iuzY2nIr0+4y9i9
|
||||
MNpqv3W47xnvgUQa5vbTbIqo2NSY24A84mF5EyjhaNgNtDlN56+qTQ6HLZNVr6pv
|
||||
eUCCWnY4GkaZUEU1M8/uNtKaZKv1WA7gJxZDQHj8+R110mPtzm1C5jqg7jSjGy9C
|
||||
8PhAwBqIXkVLNayFEtyZZobTxMH5qY1yFkI3sic7S9ZyXt3quY1Q1UT3liRteIm/
|
||||
sZHC5zEoidsHObkTeU44hqZVPkbvrfmgW01xTJjddnMPBH+yqjCCc94yCbW79j/2
|
||||
7LEmxYg=
|
||||
-----END CERTIFICATE-----`
|
||||
client1Key = `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAoKbYY9MdF2kF/nhBESIiZTdVYtA8XL9xrIZyDj9EnCiTxHiV
|
||||
bJtHXVwszqSl5TRrotPmnmAQcX3r8OCk+z+RQZ0QQj257P3kG6q4rNnOcWCS5xEd
|
||||
20jPyhQ3m+hMGfZsotNTQze1ochuQgLUN6IPyPxZkH22ia3jX4iu1eo/QxeLYHj1
|
||||
UHw43Cii9yE+j5kPUC21xmnrGKdUrB55NYLXHx6yTIqYR5znSOVB8oJi18/hwdZm
|
||||
H859DHhm0Hx1HrS+jbjI3+CMorZJ3WUyNf+CkiVLD3xYutPbxzEpwiqkG/XYzLH0
|
||||
habTcDcILo18n+o3jvem2KWBrDhyairjIDscwQIDAQABAoIBAEBSjVFqtbsp0byR
|
||||
aXvyrtLX1Ng7h++at2jca85Ihq//jyqbHTje8zPuNAKI6eNbmb0YGr5OuEa4pD9N
|
||||
ssDmMsKSoG/lRwwcm7h4InkSvBWpFShvMgUaohfHAHzsBYxfnh+TfULsi0y7c2n6
|
||||
t/2OZcOTRkkUDIITnXYiw93ibHHv2Mv2bBDu35kGrcK+c2dN5IL5ZjTjMRpbJTe2
|
||||
44RBJbdTxHBVSgoGBnugF+s2aEma6Ehsj70oyfoVpM6Aed5kGge0A5zA1JO7WCn9
|
||||
Ay/DzlULRXHjJIoRWd2NKvx5n3FNppUc9vJh2plRHalRooZ2+MjSf8HmXlvG2Hpb
|
||||
ScvmWgECgYEA1G+A/2KnxWsr/7uWIJ7ClcGCiNLdk17Pv3DZ3G4qUsU2ITftfIbb
|
||||
tU0Q/b19na1IY8Pjy9ptP7t74/hF5kky97cf1FA8F+nMj/k4+wO8QDI8OJfzVzh9
|
||||
PwielA5vbE+xmvis5Hdp8/od1Yrc/rPSy2TKtPFhvsqXjqoUmOAjDP8CgYEAwZjH
|
||||
9dt1sc2lx/rMxihlWEzQ3JPswKW9/LJAmbRBoSWF9FGNjbX7uhWtXRKJkzb8ZAwa
|
||||
88azluNo2oftbDD/+jw8b2cDgaJHlLAkSD4O1D1RthW7/LKD15qZ/oFsRb13NV85
|
||||
ZNKtwslXGbfVNyGKUVFm7fVA8vBAOUey+LKDFj8CgYEAg8WWstOzVdYguMTXXuyb
|
||||
ruEV42FJaDyLiSirOvxq7GTAKuLSQUg1yMRBIeQEo2X1XU0JZE3dLodRVhuO4EXP
|
||||
g7Dn4X7Th9HSvgvNuIacowWGLWSz4Qp9RjhGhXhezUSx2nseY6le46PmFavJYYSR
|
||||
4PBofMyt4PcyA6Cknh+KHmkCgYEAnTriG7ETE0a7v4DXUpB4TpCEiMCy5Xs2o8Z5
|
||||
ZNva+W+qLVUWq+MDAIyechqeFSvxK6gRM69LJ96lx+XhU58wJiFJzAhT9rK/g+jS
|
||||
bsHH9WOfu0xHkuHA5hgvvV2Le9B2wqgFyva4HJy82qxMxCu/VG/SMqyfBS9OWbb7
|
||||
ibQhdq0CgYAl53LUWZsFSZIth1vux2LVOsI8C3X1oiXDGpnrdlQ+K7z57hq5EsRq
|
||||
GC+INxwXbvKNqp5h0z2MvmKYPDlGVTgw8f8JjM7TkN17ERLcydhdRrMONUryZpo8
|
||||
1xTob+8blyJgfxZUIAKbMbMbIiU0WAF0rfD/eJJwS4htOW/Hfv4TGA==
|
||||
MIIEpAIBAAKCAQEA8xM5v+2QfdzfwnNT5cl+6oEy2fZoI2YG6L6c25rG0pr+yl1I
|
||||
HKdMZcvn93uat7hlbzxeOLfJRM7+QK1lLaxuppq9p+gT+1x9eG3E4X7e0pdbjrpJ
|
||||
GbvNji0hwDBLDWD8mHNq/SCk9FKtGnfZqrNB5BLw2uIKjJzVGXVlsjN6geBDm2hV
|
||||
jTSmzMr39CfLUdtvMaZhpIPJzbH+sNfp1zKavFIpmwCd77p/z0QAiQ9NaIvzv4PZ
|
||||
DDEEMUHzmVAU6bUjD8GToXaMbRiz694SU8aAwvvcdjGexdbHnfSAfLOl2wTPPxve
|
||||
PncRaa656ZeZWxY9pRCItP+v43nm7d4sAyRD4QIDAQABAoIBADE17zcgDWSt1s8z
|
||||
MgUPahZn2beu3x5rhXKRRIhhKWdx4atufy7t39WsFmZQK96OAlsmyZyJ+MFpdqf5
|
||||
csZwZmZsZYEcxw7Yhr5e2sEcQlg4NF0M8ce38cGa+X5DSK6IuBrVIw/kEAE2y7zU
|
||||
Dsk0SV63RvPJV4FoLuxcjB4rtd2c+JBduNUXQYVppz/KhsXN+9CbPbZ7wo1cB5fo
|
||||
Iu/VswvvW6EAxVx39zZcwSGdkss9XUktU8akx7T/pepIH6fwkm7uXSNez6GH9d1I
|
||||
8qOiORk/gAtqPL1TJgConyYheWMM9RbXP/IwL0BV8U4ZVG53S8jx2XpP4OJQ+k35
|
||||
WYvz8JECgYEA+9OywKOG2lMiiUB1qZfmXB80PngNsz+L6xUWkrw58gSqYZIg0xyH
|
||||
Sfr7HBo0yn/PB0oMMWPpNfYvG8/kSMIWiVlsYz9fdsUuqIvN+Kh9VF6o2wn+gnJk
|
||||
sBE3KVMofcgwgLE6eMVv2MSQlBoXhGPNlCBHS1gorQdYE82dxDPBBzsCgYEA9xpm
|
||||
c3C9LxiVbw9ZZ5D2C+vzwIG2+ZeDwKSizM1436MAnzNQgQTMzQ20uFGNBD562VjI
|
||||
rHFlZYr3KCtSIw5gvCSuox0YB64Yq/WAtGZtH9JyKRz4h4juq6iM4FT7nUwM4DF9
|
||||
3CUiDS8DGoqvCNpY50GvzSR5QVT1DKTZsMunh5MCgYEAyIWMq7pK0iQqtvG9/3o1
|
||||
8xrhxfBgsF+kcV+MZvE8jstKRIFQY+oujCkutPTlHm3hE2PSC64L8G0Em/fRRmJO
|
||||
AbZUCT9YK8HdYlZYf2zix0DM4gW2RHcEV/KNYvmVn3q9rGvzLGHCqu/yVAvmuAOk
|
||||
mhON0Z/0W7siVjp/KtEvHisCgYA/cfTaMRkyDXLY6C0BbXPvTa7xP5z2atO2U89F
|
||||
HICrkxOmzKsf5VacU6eSJ8Y4T76FLcmglSD+uHaLRsw5Ggj2Zci9MswntKi7Bjb8
|
||||
msvr/sG3EqwxSJRXWNiLBObx1UP9EFgLfTFIB0kZuIAGmuF2xyPXXUUQ5Dpi+7S1
|
||||
MyUZpwKBgQDg+AIPvk41vQ4Cz2CKrQX5/uJSW4bOhgP1yk7ruIH4Djkag3ZzTnHM
|
||||
zA9/pLzRfz1ENc5I/WaYSh92eKw3j6tUtMJlE2AbfCpgOQtRUNs3IBmzCWrY8J01
|
||||
W/8bwB+KhfFxNYwvszYsvvOq51NgahYQkgThVm38UixB3PFpEf+NiQ==
|
||||
-----END RSA PRIVATE KEY-----`
|
||||
// client 2 crt is revoked
|
||||
client2Crt = `-----BEGIN CERTIFICATE-----
|
||||
MIIEITCCAgmgAwIBAgIRAL6XTgNsdYzILd8bT2RVd/4wDQYJKoZIhvcNAQELBQAw
|
||||
EzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjEwMTAyMjEyMzIwWhcNMjIwNzAyMjEz
|
||||
MDUxWjASMRAwDgYDVQQDEwdjbGllbnQyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
|
||||
MIIBCgKCAQEA6xjW5KQR3/OFQtV5M75WINqQ4AzXSu6DhSz/yumaaQZP/UxY+6hi
|
||||
jcrFzGo9MMie/Sza8DhkXOFAl2BelUubrOeB2cl+/Gr8OCyRi2Gv6j3zCsuN/4jQ
|
||||
tNaoez/IbkDvI3l/ZpzBtnuNY2RiemGgHuORXHRVf3qVlsw+npBIRW5rM2HkO/xG
|
||||
oZjeBErWVu390Lyn+Gvk2TqQDnkutWnxUC60/zPlHhXZ4BwaFAekbSnjsSDB1YFM
|
||||
s8HwW4oBryoxdj3/+/qLrBHt75IdLw3T7/V1UDJQM3EvSQOr12w4egpldhtsC871
|
||||
nnBQZeY6qA5feffIwwg/6lJm70o6S6OX6wIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC
|
||||
A7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBTB84v5
|
||||
t9HqhLhMODbn6oYkEQt3KzAfBgNVHSMEGDAWgBTtcgjQBq/80EySIvJcN8OTorgb
|
||||
zDANBgkqhkiG9w0BAQsFAAOCAgEALGtBCve5k8tToL3oLuXp/oSik6ovIB/zq4I/
|
||||
4zNMYPU31+ZWz6aahysgx1JL1yqTa3Qm8o2tu52MbnV10dM7CIw7c/cYa+c+OPcG
|
||||
5LF97kp13X+r2axy+CmwM86b4ILaDGs2Qyai6VB6k7oFUve+av5o7aUrNFpqGCJz
|
||||
HWdtHZSVA3JMATzy0TfWanwkzreqfdw7qH0yZ9bDURlBKAVWrqnCstva9jRuv+AI
|
||||
eqxr/4Ro986TFjJdoAP3Vr16CPg7/B6GA/KmsBWJrpeJdPWq4i2gpLKvYZoy89qD
|
||||
mUZf34RbzcCtV4NvV1DadGnt4us0nvLrvS5rL2+2uWD09kZYq9RbLkvgzF/cY0fz
|
||||
i7I1bi5XQ+alWe0uAk5ZZL/D+GTRYUX1AWwCqwJxmHrMxcskMyO9pXvLyuSWRDLo
|
||||
YNBrbX9nLcfJzVCp+X+9sntTHjs4l6Cw+fLepJIgtgqdCHtbhTiv68vSM6cgb4br
|
||||
6n2xrXRKuioiWFOrTSRr+oalZh8dGJ/xvwY8IbWknZAvml9mf1VvfE7Ma5P777QM
|
||||
fsbYVTq0Y3R/5hIWsC3HA5z6MIM8L1oRe/YyhP3CTmrCHkVKyDOosGXpGz+JVcyo
|
||||
cfYkY5A3yFKB2HaCwZSfwFmRhxkrYWGEbHv3Cd9YkZs1J3hNhGFZyVMC9Uh0S85a
|
||||
6zdDidU=
|
||||
MIIEITCCAgmgAwIBAgIRANmjlDeWHEW4NSzEY2bv3hgwDQYJKoZIhvcNAQELBQAw
|
||||
EzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjIwNzA0MTU0MzUxWhcNMjQwMTA0MTU1
|
||||
MzA3WjASMRAwDgYDVQQDEwdjbGllbnQyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
|
||||
MIIBCgKCAQEAzNl7q7yS8MSaQs6zRbuqrsUuwEJ5ZH85vf7zHZKgOW3zNniXLOmH
|
||||
JdtQ3jKZQ1BCIsJFvez2GxGIMWbXaSPw4bL0J3vl5oItChsjGg34IvqcDxWuIk2a
|
||||
muRdMh7r1ryVs2ir2cQ5YHzI59BEpUWKQg3bD4yragdkb6BRc7lVgzCbrM1Eq758
|
||||
HHbaLwlsfpqOvheaum4IG113CeD/HHrw42W6g/qQWL+FHlYqV3plHZ8Bj+bhcZI5
|
||||
jdU4paGEzeY0a0NlnyH4gXGPjLKvPKFZHy4D6RiRlLHvHeiRyDtTu4wFkAiXxzGs
|
||||
E4UBbykmYUB85zgwpjaktOaoe36IM1T8CQIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC
|
||||
A7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBRdYIEk
|
||||
gxh+vTaMpAbqaPGRKGGBpTAfBgNVHSMEGDAWgBT4qzwTztNc1Ln+8QvnbGzjmOUL
|
||||
wDANBgkqhkiG9w0BAQsFAAOCAgEABSR/PbPfiNZ6FOrt91/I0g6LviwICDcuXhfr
|
||||
re4UsWp1kxXeS3CB2G71qXv3hswN8phG2hdsij0/FBEGUTLS3FTCmLmqmcVqPj3/
|
||||
677PMFDoACBKgT5iIwpnNvdD+4ROM8JFjUwy7aTWx85a5yoPFGnB+ORMfLCYjr2S
|
||||
D02KFvKuSXWCjXphqJ41cFGne4oeh/JMkN0RNArm7wTT8yWCGgO1k4OON8dphuTV
|
||||
48Wm6I9UBSWuLk1vcIlgb/8YWVwy9rBNmjOBDGuroL6PSmfZD+e9Etii0X2znZ+t
|
||||
qDpXJB7V5U0DbsBCtGM/dHaFz/LCoBYX9z6th1iPUHksUTM3RzN9L24r9/28dY/a
|
||||
shBpn5rK3ui/2mPBpO26wX14Kl/DUkdKUV9dJllSlmwo8Z0RluY9S4xnCrna/ODH
|
||||
FbhWmlTSs+odCZl6Lc0nuw+WQ2HnlTVJYBSFAGfsGQQ3pzk4DC5VynnxY0UniUgD
|
||||
WYPR8JEYa+BpH3rIQ9jmnOKWLtyc7lFPB9ab63pQBBiwRvWo+tZ2vybqjeHPuu5N
|
||||
BuKvvtu3RKKdSCnIo5Rs5zw4JYCjvlx/NVk9jtpa1lIHYHilvBmCcRX5DkE/yH/x
|
||||
IjEKhCOQpGR6D5Kkca9xNL7zNcat3bzLn+d7Wo4m09uWi9ifPdchxed0w5d9ihx1
|
||||
enqNrFI=
|
||||
-----END CERTIFICATE-----`
|
||||
client2Key = `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEA6xjW5KQR3/OFQtV5M75WINqQ4AzXSu6DhSz/yumaaQZP/UxY
|
||||
+6hijcrFzGo9MMie/Sza8DhkXOFAl2BelUubrOeB2cl+/Gr8OCyRi2Gv6j3zCsuN
|
||||
/4jQtNaoez/IbkDvI3l/ZpzBtnuNY2RiemGgHuORXHRVf3qVlsw+npBIRW5rM2Hk
|
||||
O/xGoZjeBErWVu390Lyn+Gvk2TqQDnkutWnxUC60/zPlHhXZ4BwaFAekbSnjsSDB
|
||||
1YFMs8HwW4oBryoxdj3/+/qLrBHt75IdLw3T7/V1UDJQM3EvSQOr12w4egpldhts
|
||||
C871nnBQZeY6qA5feffIwwg/6lJm70o6S6OX6wIDAQABAoIBAFatstVb1KdQXsq0
|
||||
cFpui8zTKOUiduJOrDkWzTygAmlEhYtrccdfXu7OWz0x0lvBLDVGK3a0I/TGrAzj
|
||||
4BuFY+FM/egxTVt9in6fmA3et4BS1OAfCryzUdfK6RV//8L+t+zJZ/qKQzWnugpy
|
||||
QYjDo8ifuMFwtvEoXizaIyBNLAhEp9hnrv+Tyi2O2gahPvCHsD48zkyZRCHYRstD
|
||||
NH5cIrwz9/RJgPO1KI+QsJE7Nh7stR0sbr+5TPU4fnsL2mNhMUF2TJrwIPrc1yp+
|
||||
YIUjdnh3SO88j4TQT3CIrWi8i4pOy6N0dcVn3gpCRGaqAKyS2ZYUj+yVtLO4KwxZ
|
||||
SZ1lNvECgYEA78BrF7f4ETfWSLcBQ3qxfLs7ibB6IYo2x25685FhZjD+zLXM1AKb
|
||||
FJHEXUm3mUYrFJK6AFEyOQnyGKBOLs3S6oTAswMPbTkkZeD1Y9O6uv0AHASLZnK6
|
||||
pC6ub0eSRF5LUyTQ55Jj8D7QsjXJueO8v+G5ihWhNSN9tB2UA+8NBmkCgYEA+weq
|
||||
cvoeMIEMBQHnNNLy35bwfqrceGyPIRBcUIvzQfY1vk7KW6DYOUzC7u+WUzy/hA52
|
||||
DjXVVhua2eMQ9qqtOav7djcMc2W9RbLowxvno7K5qiCss013MeWk64TCWy+WMp5A
|
||||
AVAtOliC3hMkIKqvR2poqn+IBTh1449agUJQqTMCgYEAu06IHGq1GraV6g9XpGF5
|
||||
wqoAlMzUTdnOfDabRilBf/YtSr+J++ThRcuwLvXFw7CnPZZ4TIEjDJ7xjj3HdxeE
|
||||
fYYjineMmNd40UNUU556F1ZLvJfsVKizmkuCKhwvcMx+asGrmA+tlmds4p3VMS50
|
||||
KzDtpKzLWlmU/p/RINWlRmkCgYBy0pHTn7aZZx2xWKqCDg+L2EXPGqZX6wgZDpu7
|
||||
OBifzlfM4ctL2CmvI/5yPmLbVgkgBWFYpKUdiujsyyEiQvWTUKhn7UwjqKDHtcsk
|
||||
G6p7xS+JswJrzX4885bZJ9Oi1AR2yM3sC9l0O7I4lDbNPmWIXBLeEhGMmcPKv/Kc
|
||||
91Ff4wKBgQCF3ur+Vt0PSU0ucrPVHjCe7tqazm0LJaWbPXL1Aw0pzdM2EcNcW/MA
|
||||
w0kqpr7MgJ94qhXCBcVcfPuFN9fBOadM3UBj1B45Cz3pptoK+ScI8XKno6jvVK/p
|
||||
xr5cb9VBRBtB9aOKVfuRhpatAfS2Pzm2Htae9lFn7slGPUmu2hkjDw==
|
||||
MIIEowIBAAKCAQEAzNl7q7yS8MSaQs6zRbuqrsUuwEJ5ZH85vf7zHZKgOW3zNniX
|
||||
LOmHJdtQ3jKZQ1BCIsJFvez2GxGIMWbXaSPw4bL0J3vl5oItChsjGg34IvqcDxWu
|
||||
Ik2amuRdMh7r1ryVs2ir2cQ5YHzI59BEpUWKQg3bD4yragdkb6BRc7lVgzCbrM1E
|
||||
q758HHbaLwlsfpqOvheaum4IG113CeD/HHrw42W6g/qQWL+FHlYqV3plHZ8Bj+bh
|
||||
cZI5jdU4paGEzeY0a0NlnyH4gXGPjLKvPKFZHy4D6RiRlLHvHeiRyDtTu4wFkAiX
|
||||
xzGsE4UBbykmYUB85zgwpjaktOaoe36IM1T8CQIDAQABAoIBAETHMJK0udFE8VZE
|
||||
+EQNgn0zj0LWDtQDM2vrUc04Ebu2gtZjHr7hmZLIVBqGepbzN4FcIPZnvSnRdRzB
|
||||
HsoaWyIsZ3VqUAJY6q5d9iclUY7M/eDCsripvaML0Y6meyCaKNkX57sx+uG+g+Xx
|
||||
M1saQhVzeX17CYKMANjJxw9HxsJI0aBPyiBbILHMwfRfsJU8Ou72HH1sIQuPdH2H
|
||||
/c9ru8YZAno6oVq1zuC/pCis+h50U9HzTnt3/4NNS6cWG/y2YLztCvm9uGo4MTd/
|
||||
mA9s4cxVhvQW6gCDHgGn6zj661OL/d2rpak1eWizhZvZ8jsIN/sM87b0AJeVT4zH
|
||||
6xA3egECgYEA1nI5EsCetQbFBp7tDovSp3fbitwoQtdtHtLn2u4DfvmbLrgSoq0Z
|
||||
L+9N13xML/l8lzWai2gI69uA3c2+y1O64LkaiSeDqbeBp9b6fKMlmwIVbklEke1w
|
||||
XVTIWOYTTF5/8+tUOlsgme5BhLAWnQ7+SoitzHtl5e1vEYaAGamE2DECgYEA9Is2
|
||||
BbTk2YCqkcsB7D9q95JbY0SZpecvTv0rLR+acz3T8JrAASdmvqdBOlPWc+0ZaEdS
|
||||
PcJaOEw3yxYJ33cR/nLBaR2/Uu5qQebyPALs3B2pjjTFdGvcpeFxO55fowwsfR/e
|
||||
0H+HeiFj5Y4S+kFWT+3FRmJ6GUB828LJYaVhQ1kCgYEA1bdsTdYN1Vfzz89fbZnH
|
||||
zQLUl6UlssfDhm6mhzeh4E+eaocke1+LtIwHxfOocj9v/bp8VObPzU8rNOIxfa3q
|
||||
lr+jRIFO5DtwSfckGEb32W3QMeNvJQe/biRqrr5NCVU8q7kibi4XZZFfVn+vacNh
|
||||
hqKEoz9vpCBnCs5CqFCbhmECgYAG8qWYR+lwnI08Ey58zdh2LDxYd6x94DGh5uOB
|
||||
JrK2r30ECwGFht8Ob6YUyCkBpizgn5YglxMFInU7Webx6GokdpI0MFotOwTd1nfv
|
||||
aI3eOyGEHs+1XRMpy1vyO6+v7DqfW3ZzKgxpVeWGsiCr54tSPgkq1MVvTju96qza
|
||||
D17SEQKBgCKC0GjDjnt/JvujdzHuBt1sWdOtb+B6kQvA09qVmuDF/Dq36jiaHDjg
|
||||
XMf5HU3ThYqYn3bYypZZ8nQ7BXVh4LqGNqG29wR4v6l+dLO6odXnLzfApGD9e+d4
|
||||
2tmlLP54LaN35hQxRjhT8lCN0BkrNF44+bh8frwm/kuxSd8wT2S+
|
||||
-----END RSA PRIVATE KEY-----`
|
||||
testFileName = "test_file_ftp.dat"
|
||||
testDLFileName = "test_download_ftp.dat"
|
||||
|
|
46
go.mod
46
go.mod
|
@ -4,25 +4,25 @@ go 1.18
|
|||
|
||||
require (
|
||||
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/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
||||
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/config v1.15.12
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.12.7
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.7
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.17
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.7
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.26.12
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.12
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.16.8
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.7
|
||||
github.com/aws/aws-sdk-go-v2/config v1.15.13
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.12.8
|
||||
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.19
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.8
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.27.1
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.13
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.16.9
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.2.14
|
||||
github.com/coreos/go-oidc/v3 v3.2.0
|
||||
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
|
||||
github.com/fclairamb/ftpserverlib v0.18.1-0.20220515214847-f96d31ec626e
|
||||
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/jwtauth/v5 v5.0.2
|
||||
github.com/go-chi/render v1.0.1
|
||||
|
@ -52,13 +52,13 @@ require (
|
|||
github.com/rs/xid v1.4.0
|
||||
github.com/rs/zerolog v1.27.0
|
||||
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/cobra v1.5.0
|
||||
github.com/spf13/viper v1.12.0
|
||||
github.com/stretchr/testify v1.8.0
|
||||
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/xhit/go-simple-mail/v2 v2.11.0
|
||||
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/net v0.0.0-20220624214902-1bab6f366d9e
|
||||
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
|
||||
google.golang.org/api v0.86.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
|
@ -80,15 +80,15 @@ require (
|
|||
cloud.google.com/go/iam v0.3.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/internal/configsources v1.1.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.4 // 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.8 // 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.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/checksum v1.1.8 // 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/s3shared v1.13.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.11.10 // 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.8 // 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.11 // indirect
|
||||
github.com/aws/smithy-go v1.12.0 // indirect
|
||||
github.com/beorn7/perks 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/xerrors v0.0.0-20220609144429-65e65417b02f // 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/protobuf v1.28.0 // indirect
|
||||
gopkg.in/ini.v1 v1.66.6 // indirect
|
||||
|
|
93
go.sum
93
go.sum
|
@ -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/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 v1.1.0 h1:Ut0ZGdOwJDw0npYEg+TLlPls3Pq6JiZaP2/aGKir7Zw=
|
||||
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 h1:tz19qLF65vuu2ibfTqGVJxG/zZAI27NEIIbvAOQwYbw=
|
||||
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 v1.0.0 h1:Yoicul8bnVdQrhDMTHxdEckRGX01XvwXDHUT9zYZ3k0=
|
||||
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.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.6 h1:kzafGZYwkwVgLZ2zEX7P+vTwLli6uIMXF8aGjunN6UI=
|
||||
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 h1:zfBwXus3u14OszRxGcqCDS4MfMCv10e8SMJ2r8Xm0Ns=
|
||||
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.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/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.12/go.mod h1:oxRNnH11J580bxDEXyfTqfB3Auo2fxzhV052LD4HnyA=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.15.13 h1:CJH9zn/Enst7lDiGpoguVt0lZr5HcpNVlRJWbJ6qreo=
|
||||
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.12.7 h1:e2DcCR0gP+T2zVj5eQPMQoRdxo+vd2p9BkpJ72BdyzA=
|
||||
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 h1:niTa7zc7uyOP2ufri0jPESBt1h9yP3Zc0q+xzih3h8o=
|
||||
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.7 h1:8yi2ORCwXpXEPnj0vP3DjYhejwDQD/5klgBoxXcKOxY=
|
||||
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 h1:VfBdn2AxwMbFyJN/lF/xuT3SakomJ86PZu3rCxb5K0s=
|
||||
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.17 h1:9Y+OvoIvC8KocGNqbbBNDvMu0zsIgzKg3r+ZllSuH5Y=
|
||||
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 h1:WfCYqsAADDRNCQQ5LGcrlqbR7SK3PYrP/UCh7qNGBQM=
|
||||
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.13 h1:WuQ1yGs3TMJgxpGVLspcsU/5q1omSA0SG6Cu0yZ4jkM=
|
||||
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 h1:2C0pYHcUBmdzPj+EKNC4qj97oK6yjrUhc1KoSodglvk=
|
||||
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.7 h1:mCeDDYeDXp3loo/xKi7nkx34eeh7q3n1mUBtzptsj8c=
|
||||
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 h1:2J+jdlBJWEmTyAwC82Ym68xCykIvnSnIN18b8xHGlcc=
|
||||
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.14 h1:bJv4Y9QOiW0GZPStgLgpGrpdfRDSR3XM4V4M3YCQRZo=
|
||||
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/v4a v1.0.4 h1:wusoY1MJ9JNrPoX3n4kxY4MTIUivCiXvTYQbYh59yxs=
|
||||
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/ini v1.3.15 h1:QquxR7NH3ULBsKC+NoTpilzbKKS+5AELfNREInbhvas=
|
||||
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.5 h1:tEEHn+PGAxRVqMPEhtU8oCSW/1Ge3zP5nUgPrGQNUPs=
|
||||
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.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/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.8/go.mod h1:a1BSeQI9IVr1j5Dwn73cdAKi4MdizTaV9YovUaHefGI=
|
||||
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.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.7 h1:M7/BzQNsu0XXiJRe3gUn8UA8tExF6kLMAfvo5PT/KJY=
|
||||
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 h1:oKnAXxSF2FUvfgw8uzU/v9OTYorJJZ8eBmWhr9TWVVQ=
|
||||
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.7 h1:imb0NhTQZaTDSAQvgFyiZbKTwl0F+AkZL1ZNoEHtuQc=
|
||||
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 h1:TlN1UC39A0LUNoD51ubO5h32haznA+oVe15jO9O4Lj0=
|
||||
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/marketplacemetering v1.13.7 h1:/5I4p9aytzW1pQfQaCYAqTUzVE48jgJHVFfQVTU4LB8=
|
||||
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 h1:J1muHmn3xAYD1KQpngUX0lCVMu1hJeLbZIihiAwH7ME=
|
||||
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.12 h1:/JTTdNObz+GygQqnbdBzummuxFIcuB6hbra1mqS+Wic=
|
||||
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 h1:OKQIQ0QhEBmGr2LfT952meIZz3ujrPYnxH+dO/5ldnI=
|
||||
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.12 h1:Pq2GrbfG74dX/JhY/O+bWBz7DUzdgTvqugofqTWLACQ=
|
||||
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 h1:9hFlfWKP1+u3js8IhRGf3M+S4MSoDK2v3bqIndGEpxU=
|
||||
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/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/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.10/go.mod h1:UHxA35uPrCykRySBV5iSPZhZRlYnWSS2c/aaZVsoU94=
|
||||
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.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.8 h1:GLGfpqX+1bmjNvUJkwB1ZaDpNFXQwJ3z9RkQDA58OBY=
|
||||
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 h1:yOfILxyjmtr2ubRkRJldlHDFBhf5vw4CzhbwWIBmimQ=
|
||||
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.12.0 h1:gXpeZel/jPoWQ7OEmLIgCUnhkFftqNfwWUwAHSlp1v0=
|
||||
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-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/go-acme/lego/v4 v4.7.0 h1:f5/9EigoIC3Lu14XUsbDzlJ1ZcTYBN3zu/0TJExQaOc=
|
||||
github.com/go-acme/lego/v4 v4.7.0/go.mod h1:hoPWeY+jooDbgbe5GUqHTGRGdENDhrkiypiDCAqgmmg=
|
||||
github.com/go-acme/lego/v4 v4.8.0 h1:XienkuT6ZKHe0DE/LXeGP4ZY+ft+7ZMlqtiJ7XJs2pI=
|
||||
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.8-0.20220512131524-9e71a0d4b3d6 h1:+fT7oFUOersdx+u7uIxOjabDVGxg+qqNV6kRdAXIvaQ=
|
||||
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/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/shirou/gopsutil/v3 v3.22.5 h1:atX36I/IXgFiB81687vSiBI5zrMsxcIBkP9cQMJQoJA=
|
||||
github.com/shirou/gopsutil/v3 v3.22.5/go.mod h1:so9G9VzeHt/hsd0YwqprnjHnfARAUktauykSbr+y2gA=
|
||||
github.com/shirou/gopsutil/v3 v3.22.6 h1:FnHOFOh+cYAM0C30P+zysPISzlknLC5Z1G4EAElznfQ=
|
||||
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 v1.2.0/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.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.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/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
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/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/unrolled/secure v1.10.0 h1:TBNP42z2AB+2pW9PR6vdbqhlQuv1iTeSVzK1qHjOBzA=
|
||||
github.com/unrolled/secure v1.10.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
|
||||
github.com/unrolled/secure v1.11.0 h1:fjkKhD/MsQnlmz/au+MmFptCFNhvf5iv04ALkdCXRCI=
|
||||
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/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ=
|
||||
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-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-20220627191245-f75cf1eec38b h1:2n253B2r0pYSmEV+UNCQoPfU/FiaizQEK5Gu4Bq4JE8=
|
||||
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d h1:/m5NbqQelATgoSPVC2Z23sR4kVNokFwDDyWh/3rGY+I=
|
||||
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-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
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-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-20220630160836-4327a74d660d h1:9IbUS1WUO1OPTBCp32JFwX9MqyT5pc3UWXH8plPX8is=
|
||||
google.golang.org/genproto v0.0.0-20220630160836-4327a74d660d/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
|
||||
google.golang.org/genproto v0.0.0-20220708155623-50e5f4832e73 h1:sdZWfcGN37Dv0QWIhuasQGMzAQJOL2oqnvot4/kPgfQ=
|
||||
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.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
|
|
235
httpd/api_eventrule.go
Normal file
235
httpd/api_eventrule.go
Normal 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)
|
||||
}
|
|
@ -13,7 +13,6 @@ import (
|
|||
|
||||
"github.com/go-chi/render"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/common"
|
||||
"github.com/drakkan/sftpgo/v2/dataprovider"
|
||||
"github.com/drakkan/sftpgo/v2/logger"
|
||||
"github.com/drakkan/sftpgo/v2/util"
|
||||
|
@ -190,7 +189,15 @@ func restoreBackup(content []byte, inputFile string, scanQuota, mode int, execut
|
|||
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))
|
||||
|
||||
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)
|
||||
}
|
||||
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)
|
||||
go doFolderQuotaScan(folder) //nolint:errcheck
|
||||
}
|
||||
|
@ -280,6 +287,54 @@ func RestoreShares(shares []dataprovider.Share, inputFile string, mode int, exec
|
|||
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
|
||||
func RestoreAPIKeys(apiKeys []dataprovider.APIKey, inputFile string, mode int, executor, ipAddress string) error {
|
||||
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)
|
||||
}
|
||||
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)
|
||||
go doUserQuotaScan(user) //nolint:errcheck
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
|
||||
"github.com/go-chi/render"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/common"
|
||||
"github.com/drakkan/sftpgo/v2/dataprovider"
|
||||
"github.com/drakkan/sftpgo/v2/logger"
|
||||
"github.com/drakkan/sftpgo/v2/vfs"
|
||||
|
@ -30,12 +29,12 @@ type transferQuotaUsage struct {
|
|||
|
||||
func getUsersQuotaScans(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
render.JSON(w, r, common.QuotaScans.GetUsersQuotaScans())
|
||||
render.JSON(w, r, dataprovider.QuotaScans.GetUsersQuotaScans())
|
||||
}
|
||||
|
||||
func getFoldersQuotaScans(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
render.JSON(w, r, common.QuotaScans.GetVFoldersQuotaScans())
|
||||
render.JSON(w, r, dataprovider.QuotaScans.GetVFoldersQuotaScans())
|
||||
}
|
||||
|
||||
func updateUserQuotaUsage(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -128,11 +127,11 @@ func doUpdateUserQuotaUsage(w http.ResponseWriter, r *http.Request, username str
|
|||
"", http.StatusBadRequest)
|
||||
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)
|
||||
return
|
||||
}
|
||||
defer common.QuotaScans.RemoveUserQuotaScan(user.Username)
|
||||
defer dataprovider.QuotaScans.RemoveUserQuotaScan(user.Username)
|
||||
err = dataprovider.UpdateUserQuota(&user, usage.UsedQuotaFiles, usage.UsedQuotaSize, mode == quotaUpdateModeReset)
|
||||
if err != nil {
|
||||
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))
|
||||
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)
|
||||
return
|
||||
}
|
||||
defer common.QuotaScans.RemoveVFolderQuotaScan(folder.Name)
|
||||
defer dataprovider.QuotaScans.RemoveVFolderQuotaScan(folder.Name)
|
||||
err = dataprovider.UpdateVirtualFolderQuota(&folder, usage.UsedQuotaFiles, usage.UsedQuotaSize, mode == quotaUpdateModeReset)
|
||||
if err != nil {
|
||||
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))
|
||||
return
|
||||
}
|
||||
if !common.QuotaScans.AddUserQuotaScan(user.Username) {
|
||||
sendAPIResponse(w, r, err, fmt.Sprintf("Another scan is already in progress for user %#v", username),
|
||||
if !dataprovider.QuotaScans.AddUserQuotaScan(user.Username) {
|
||||
sendAPIResponse(w, r, nil, fmt.Sprintf("Another scan is already in progress for user %#v", username),
|
||||
http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
@ -199,7 +198,7 @@ func doStartFolderQuotaScan(w http.ResponseWriter, r *http.Request, name string)
|
|||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
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),
|
||||
http.StatusConflict)
|
||||
return
|
||||
|
@ -209,7 +208,7 @@ func doStartFolderQuotaScan(w http.ResponseWriter, r *http.Request, name string)
|
|||
}
|
||||
|
||||
func doUserQuotaScan(user dataprovider.User) error {
|
||||
defer common.QuotaScans.RemoveUserQuotaScan(user.Username)
|
||||
defer dataprovider.QuotaScans.RemoveUserQuotaScan(user.Username)
|
||||
numFiles, size, err := user.ScanQuota()
|
||||
if err != nil {
|
||||
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 {
|
||||
defer common.QuotaScans.RemoveVFolderQuotaScan(folder.Name)
|
||||
defer dataprovider.QuotaScans.RemoveVFolderQuotaScan(folder.Name)
|
||||
f := vfs.VirtualFolder{
|
||||
BaseVirtualFolder: folder,
|
||||
VirtualPath: "/",
|
||||
|
|
|
@ -574,7 +574,7 @@ func handleForgotPassword(r *http.Request, username string, isAdmin bool) error
|
|||
return util.NewGenericError("Unable to render password reset template")
|
||||
}
|
||||
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",
|
||||
err, time.Since(startTime))
|
||||
return util.NewGenericError(fmt.Sprintf("Unable to send confirmation code via email: %v", err))
|
||||
|
|
|
@ -74,6 +74,8 @@ const (
|
|||
fsEventsPath = "/api/v2/events/fs"
|
||||
providerEventsPath = "/api/v2/events/provider"
|
||||
sharesPath = "/api/v2/shares"
|
||||
eventActionsPath = "/api/v2/eventactions"
|
||||
eventRulesPath = "/api/v2/eventrules"
|
||||
healthzPath = "/healthz"
|
||||
robotsTxtPath = "/robots.txt"
|
||||
webRootPathDefault = "/"
|
||||
|
@ -107,6 +109,10 @@ const (
|
|||
webAdminResetPwdPathDefault = "/web/admin/reset-password"
|
||||
webAdminProfilePathDefault = "/web/admin/profile"
|
||||
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"
|
||||
webAdminTOTPValidatePathDefault = "/web/admin/totp/validate"
|
||||
webAdminTOTPSavePathDefault = "/web/admin/totp/save"
|
||||
|
@ -185,6 +191,10 @@ var (
|
|||
webQuotaScanPath string
|
||||
webAdminProfilePath string
|
||||
webAdminMFAPath string
|
||||
webAdminEventRulesPath string
|
||||
webAdminEventRulePath string
|
||||
webAdminEventActionsPath string
|
||||
webAdminEventActionPath string
|
||||
webAdminTOTPGeneratePath string
|
||||
webAdminTOTPValidatePath string
|
||||
webAdminTOTPSavePath string
|
||||
|
@ -755,6 +765,7 @@ func (c *Conf) Initialize(configDir string, isShared int) error {
|
|||
return
|
||||
}
|
||||
server := newHttpdServer(b, staticFilesPath, c.SigningPassphrase, c.Cors, openAPIPath)
|
||||
server.setShared(isShared)
|
||||
|
||||
exitChannel <- server.listenAndServe()
|
||||
}(binding)
|
||||
|
@ -890,6 +901,10 @@ func updateWebAdminURLs(baseURL string) {
|
|||
webAdminResetPwdPath = path.Join(baseURL, webAdminResetPwdPathDefault)
|
||||
webAdminProfilePath = path.Join(baseURL, webAdminProfilePathDefault)
|
||||
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)
|
||||
webAdminTOTPValidatePath = path.Join(baseURL, webAdminTOTPValidatePathDefault)
|
||||
webAdminTOTPSavePath = path.Join(baseURL, webAdminTOTPSavePathDefault)
|
||||
|
|
1439
httpd/httpd_test.go
1439
httpd/httpd_test.go
File diff suppressed because it is too large
Load diff
|
@ -611,6 +611,36 @@ func TestInvalidToken(t *testing.T) {
|
|||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
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()
|
||||
server.handleWebAddAdminPost(rr, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
|
@ -626,6 +656,26 @@ func TestInvalidToken(t *testing.T) {
|
|||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
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()
|
||||
server.handleWebClientTwoFactorRecoveryPost(rr, req)
|
||||
assert.Equal(t, http.StatusNotFound, rr.Code)
|
||||
|
@ -836,6 +886,7 @@ func TestCreateTokenError(t *testing.T) {
|
|||
rr = httptest.NewRecorder()
|
||||
server.handleWebAdminLoginPost(rr, req)
|
||||
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
|
||||
|
||||
req, _ = http.NewRequest(http.MethodGet, webAdminLoginPath+"?a=a%C3%A1%G2", nil)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr = httptest.NewRecorder()
|
||||
|
@ -848,6 +899,16 @@ func TestCreateTokenError(t *testing.T) {
|
|||
_, err := getAdminFromPostFields(req)
|
||||
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.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr = httptest.NewRecorder()
|
||||
|
@ -1330,7 +1391,7 @@ func TestQuotaScanInvalidFs(t *testing.T) {
|
|||
Provider: sdk.S3FilesystemProvider,
|
||||
},
|
||||
}
|
||||
common.QuotaScans.AddUserQuotaScan(user.Username)
|
||||
dataprovider.QuotaScans.AddUserQuotaScan(user.Username)
|
||||
err := doUserQuotaScan(user)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ type httpdServer struct {
|
|||
enableWebAdmin bool
|
||||
enableWebClient bool
|
||||
renderOpenAPI bool
|
||||
isShared int
|
||||
router *chi.Mux
|
||||
tokenAuth *jwtauth.JWTAuth
|
||||
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 {
|
||||
s.initializeRouter()
|
||||
httpServer := &http.Server{
|
||||
|
@ -1259,6 +1264,16 @@ func (s *httpdServer) initializeRouter() {
|
|||
Put(apiKeysPath+"/{id}", updateAPIKey)
|
||||
router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
|
||||
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)
|
||||
|
@ -1504,7 +1519,8 @@ func (s *httpdServer) setupWebAdminRoutes() {
|
|||
router.With(verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminTOTPGeneratePath, generateTOTPSecret)
|
||||
router.With(verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminTOTPValidatePath, validateTOTPPasscode)
|
||||
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(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.PermAdminManageDefender)).Delete(webDefenderHostsPath+"/{id}",
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,11 +43,11 @@ const (
|
|||
folderPageModeTemplate
|
||||
)
|
||||
|
||||
type groupPageMode int
|
||||
type genericPageMode int
|
||||
|
||||
const (
|
||||
groupPageModeAdd groupPageMode = iota + 1
|
||||
groupPageModeUpdate
|
||||
genericPageModeAdd genericPageMode = iota + 1
|
||||
genericPageModeUpdate
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -65,6 +65,10 @@ const (
|
|||
templateGroup = "group.html"
|
||||
templateFolders = "folders.html"
|
||||
templateFolder = "folder.html"
|
||||
templateEventRules = "eventrules.html"
|
||||
templateEventRule = "eventrule.html"
|
||||
templateEventActions = "eventactions.html"
|
||||
templateEventAction = "eventaction.html"
|
||||
templateMessage = "message.html"
|
||||
templateStatus = "status.html"
|
||||
templateLogin = "login.html"
|
||||
|
@ -80,6 +84,8 @@ const (
|
|||
pageStatusTitle = "Status"
|
||||
pageFoldersTitle = "Folders"
|
||||
pageGroupsTitle = "Groups"
|
||||
pageEventRulesTitle = "Event rules"
|
||||
pageEventActionsTitle = "Event actions"
|
||||
pageProfileTitle = "My profile"
|
||||
pageChangePwdTitle = "Change password"
|
||||
pageMaintenanceTitle = "Maintenance"
|
||||
|
@ -114,6 +120,10 @@ type basePage struct {
|
|||
ProfileURL string
|
||||
ChangePwdURL string
|
||||
MFAURL string
|
||||
EventRulesURL string
|
||||
EventRuleURL string
|
||||
EventActionsURL string
|
||||
EventActionURL string
|
||||
FolderQuotaScanURL string
|
||||
StatusURL string
|
||||
MaintenanceURL string
|
||||
|
@ -123,11 +133,14 @@ type basePage struct {
|
|||
ConnectionsTitle string
|
||||
FoldersTitle string
|
||||
GroupsTitle string
|
||||
EventRulesTitle string
|
||||
EventActionsTitle string
|
||||
StatusTitle string
|
||||
MaintenanceTitle string
|
||||
DefenderTitle string
|
||||
Version string
|
||||
CSRFToken string
|
||||
IsEventManagerPage bool
|
||||
HasDefender bool
|
||||
HasExternalLogin bool
|
||||
LoggedAdmin *dataprovider.Admin
|
||||
|
@ -154,6 +167,16 @@ type groupsPage struct {
|
|||
Groups []dataprovider.Group
|
||||
}
|
||||
|
||||
type eventRulesPage struct {
|
||||
basePage
|
||||
Rules []dataprovider.EventRule
|
||||
}
|
||||
|
||||
type eventActionsPage struct {
|
||||
basePage
|
||||
Actions []dataprovider.BaseEventAction
|
||||
}
|
||||
|
||||
type connectionsPage struct {
|
||||
basePage
|
||||
Connections []common.ConnectionStatus
|
||||
|
@ -183,7 +206,6 @@ type userPage struct {
|
|||
TwoFactorProtocols []string
|
||||
WebClientOptions []string
|
||||
RootDirPerms []string
|
||||
RedactedSecret string
|
||||
Mode userPageMode
|
||||
VirtualFolders []vfs.BaseVirtualFolder
|
||||
Groups []dataprovider.Group
|
||||
|
@ -253,7 +275,7 @@ type groupPage struct {
|
|||
basePage
|
||||
Group *dataprovider.Group
|
||||
Error string
|
||||
Mode groupPageMode
|
||||
Mode genericPageMode
|
||||
ValidPerms []string
|
||||
ValidLoginMethods []string
|
||||
ValidProtocols []string
|
||||
|
@ -263,6 +285,30 @@ type groupPage struct {
|
|||
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 {
|
||||
basePage
|
||||
Error string
|
||||
|
@ -341,6 +387,26 @@ func loadAdminTemplates(templatesPath string) {
|
|||
filepath.Join(templatesPath, templateAdminDir, templateSharedComponents),
|
||||
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{
|
||||
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
|
||||
filepath.Join(templatesPath, templateAdminDir, templateBase),
|
||||
|
@ -408,6 +474,10 @@ func loadAdminTemplates(templatesPath string) {
|
|||
groupTmpl := util.LoadTemplate(fsBaseTpl, groupPaths...)
|
||||
foldersTmpl := util.LoadTemplate(nil, foldersPaths...)
|
||||
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...)
|
||||
loginTmpl := util.LoadTemplate(nil, loginPaths...)
|
||||
profileTmpl := util.LoadTemplate(nil, profilePaths...)
|
||||
|
@ -431,6 +501,10 @@ func loadAdminTemplates(templatesPath string) {
|
|||
adminTemplates[templateGroup] = groupTmpl
|
||||
adminTemplates[templateFolders] = foldersTmpl
|
||||
adminTemplates[templateFolder] = folderTmpl
|
||||
adminTemplates[templateEventRules] = eventRulesTmpl
|
||||
adminTemplates[templateEventRule] = eventRuleTmpl
|
||||
adminTemplates[templateEventActions] = eventActionsTmpl
|
||||
adminTemplates[templateEventAction] = eventActionTmpl
|
||||
adminTemplates[templateStatus] = statusTmpl
|
||||
adminTemplates[templateLogin] = loginTmpl
|
||||
adminTemplates[templateProfile] = profileTmpl
|
||||
|
@ -445,6 +519,22 @@ func loadAdminTemplates(templatesPath string) {
|
|||
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 {
|
||||
var csrfToken string
|
||||
if currentURL != "" {
|
||||
|
@ -468,6 +558,10 @@ func (s *httpdServer) getBasePageData(title, currentURL string, r *http.Request)
|
|||
ProfileURL: webAdminProfilePath,
|
||||
ChangePwdURL: webChangeAdminPwdPath,
|
||||
MFAURL: webAdminMFAPath,
|
||||
EventRulesURL: webAdminEventRulesPath,
|
||||
EventRuleURL: webAdminEventRulePath,
|
||||
EventActionsURL: webAdminEventActionsPath,
|
||||
EventActionURL: webAdminEventActionPath,
|
||||
QuotaScanURL: webQuotaScanPath,
|
||||
ConnectionsURL: webConnectionsPath,
|
||||
StatusURL: webStatusPath,
|
||||
|
@ -479,11 +573,14 @@ func (s *httpdServer) getBasePageData(title, currentURL string, r *http.Request)
|
|||
ConnectionsTitle: pageConnectionsTitle,
|
||||
FoldersTitle: pageFoldersTitle,
|
||||
GroupsTitle: pageGroupsTitle,
|
||||
EventRulesTitle: pageEventRulesTitle,
|
||||
EventActionsTitle: pageEventActionsTitle,
|
||||
StatusTitle: pageStatusTitle,
|
||||
MaintenanceTitle: pageMaintenanceTitle,
|
||||
DefenderTitle: pageDefenderTitle,
|
||||
Version: version.GetAsString(),
|
||||
LoggedAdmin: getAdminFromToken(r),
|
||||
IsEventManagerPage: isEventManagerResource(currentURL),
|
||||
HasDefender: common.Config.DefenderConfig.Enabled,
|
||||
HasExternalLogin: isLoggedInWithOIDC(r),
|
||||
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,
|
||||
mode groupPageMode, error string,
|
||||
mode genericPageMode, error string,
|
||||
) {
|
||||
folders, err := s.getWebVirtualFolders(w, r, defaultQueryLimit, true)
|
||||
if err != nil {
|
||||
|
@ -731,10 +828,10 @@ func (s *httpdServer) renderGroupPage(w http.ResponseWriter, r *http.Request, gr
|
|||
group.UserSettings.FsConfig.RedactedSecret = redactedSecret
|
||||
var title, currentURL string
|
||||
switch mode {
|
||||
case groupPageModeAdd:
|
||||
case genericPageModeAdd:
|
||||
title = "Add a new group"
|
||||
currentURL = webGroupPath
|
||||
case groupPageModeUpdate:
|
||||
case genericPageModeUpdate:
|
||||
title = "Update group"
|
||||
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)
|
||||
}
|
||||
|
||||
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,
|
||||
mode folderPageMode, error string,
|
||||
) {
|
||||
|
@ -1630,6 +1792,204 @@ func getGroupFromPostFields(r *http.Request) (dataprovider.Group, error) {
|
|||
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) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
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) {
|
||||
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) {
|
||||
|
@ -2462,7 +2822,7 @@ func (s *httpdServer) handleWebAddGroupPost(w http.ResponseWriter, r *http.Reque
|
|||
}
|
||||
group, err := getGroupFromPostFields(r)
|
||||
if err != nil {
|
||||
s.renderGroupPage(w, r, group, groupPageModeAdd, err.Error())
|
||||
s.renderGroupPage(w, r, group, genericPageModeAdd, err.Error())
|
||||
return
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
s.renderGroupPage(w, r, group, groupPageModeAdd, err.Error())
|
||||
s.renderGroupPage(w, r, group, genericPageModeAdd, err.Error())
|
||||
return
|
||||
}
|
||||
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")
|
||||
group, err := dataprovider.GroupExists(name)
|
||||
if err == nil {
|
||||
s.renderGroupPage(w, r, group, groupPageModeUpdate, "")
|
||||
s.renderGroupPage(w, r, group, genericPageModeUpdate, "")
|
||||
} else if _, ok := err.(*util.RecordNotFoundError); ok {
|
||||
s.renderNotFoundPage(w, r, err)
|
||||
} else {
|
||||
|
@ -2509,7 +2869,7 @@ func (s *httpdServer) handleWebUpdateGroupPost(w http.ResponseWriter, r *http.Re
|
|||
}
|
||||
updatedGroup, err := getGroupFromPostFields(r)
|
||||
if err != nil {
|
||||
s.renderGroupPage(w, r, group, groupPageModeUpdate, err.Error())
|
||||
s.renderGroupPage(w, r, group, genericPageModeUpdate, err.Error())
|
||||
return
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
s.renderGroupPage(w, r, updatedGroup, groupPageModeUpdate, err.Error())
|
||||
s.renderGroupPage(w, r, updatedGroup, genericPageModeUpdate, err.Error())
|
||||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -46,6 +46,8 @@ const (
|
|||
apiKeysPath = "/api/v2/apikeys"
|
||||
retentionBasePath = "/api/v2/retention/users"
|
||||
retentionChecksPath = "/api/v2/retention/users/checks"
|
||||
eventActionsPath = "/api/v2/eventactions"
|
||||
eventRulesPath = "/api/v2/eventrules"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -598,9 +600,227 @@ func GetAPIKeyByID(keyID string, expectedStatusCode int) (dataprovider.APIKey, [
|
|||
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.
|
||||
func GetQuotaScans(expectedStatusCode int) ([]common.ActiveQuotaScan, []byte, error) {
|
||||
var quotaScans []common.ActiveQuotaScan
|
||||
func GetQuotaScans(expectedStatusCode int) ([]dataprovider.ActiveQuotaScan, []byte, error) {
|
||||
var quotaScans []dataprovider.ActiveQuotaScan
|
||||
var body []byte
|
||||
resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(quotaScanPath), nil, "", getDefaultToken())
|
||||
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.
|
||||
func GetFoldersQuotaScans(expectedStatusCode int) ([]common.ActiveVirtualFolderQuotaScan, []byte, error) {
|
||||
var quotaScans []common.ActiveVirtualFolderQuotaScan
|
||||
func GetFoldersQuotaScans(expectedStatusCode int) ([]dataprovider.ActiveVirtualFolderQuotaScan, []byte, error) {
|
||||
var quotaScans []dataprovider.ActiveVirtualFolderQuotaScan
|
||||
var body []byte
|
||||
resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(quotaScanVFolderPath), nil, "", getDefaultToken())
|
||||
if err != nil {
|
||||
|
@ -1087,7 +1307,149 @@ func getResponseBody(resp *http.Response) ([]byte, error) {
|
|||
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 actual.ID <= 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 {
|
||||
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,
|
||||
actual.UserSettings.BaseGroupUserSettings); err != nil {
|
||||
return err
|
||||
|
@ -1202,12 +1601,12 @@ func checkAdmin(expected, actual *dataprovider.Admin) error {
|
|||
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 {
|
||||
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 {
|
||||
if !util.Contains(actual.Filters.AllowList, v) {
|
||||
return errors.New("allow list content mismatch")
|
||||
|
@ -1748,6 +2147,87 @@ func compareUserFilePatternsFilters(expected sdk.BaseUserFilters, actual sdk.Bas
|
|||
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 {
|
||||
if expected.HomeDir != actual.HomeDir {
|
||||
return errors.New("home dir mismatch")
|
||||
|
|
|
@ -16,6 +16,7 @@ tags:
|
|||
- name: metadata
|
||||
- name: user APIs
|
||||
- name: public shares
|
||||
- name: event manager
|
||||
info:
|
||||
title: SFTPGo
|
||||
description: |
|
||||
|
@ -1450,7 +1451,7 @@ paths:
|
|||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
message: User updated
|
||||
message: Folder updated
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
|
@ -1625,7 +1626,7 @@ paths:
|
|||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
message: User updated
|
||||
message: Group updated
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
|
@ -1641,7 +1642,7 @@ paths:
|
|||
delete:
|
||||
tags:
|
||||
- groups
|
||||
summary: Delete
|
||||
summary: Delete group
|
||||
description: Deletes an existing group
|
||||
operationId: delete_group
|
||||
responses:
|
||||
|
@ -1652,7 +1653,357 @@ paths:
|
|||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
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':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
|
@ -3942,6 +4293,7 @@ components:
|
|||
- retention_checks
|
||||
- metadata_checks
|
||||
- view_events
|
||||
- manage_event_rules
|
||||
description: |
|
||||
Admin permissions:
|
||||
* `*` - all permissions are granted
|
||||
|
@ -3961,6 +4313,7 @@ components:
|
|||
* `retention_checks` - view and start retention checks is allowed
|
||||
* `metadata_checks` - view and start metadata checks is allowed
|
||||
* `view_events` - view and search filesystem and provider events is allowed
|
||||
* `manage_event_rules` - manage event actions and rules is allowed
|
||||
FsProviders:
|
||||
type: integer
|
||||
enum:
|
||||
|
@ -3980,6 +4333,36 @@ components:
|
|||
* `4` - Local filesystem encrypted
|
||||
* `5` - SFTP
|
||||
* `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:
|
||||
type: string
|
||||
enum:
|
||||
|
@ -5289,20 +5672,6 @@ components:
|
|||
type: boolean
|
||||
mfa:
|
||||
$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:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -5574,6 +5943,269 @@ components:
|
|||
type: string
|
||||
instance_id:
|
||||
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:
|
||||
type: object
|
||||
properties:
|
||||
|
|
|
@ -351,5 +351,13 @@ func (s *Service) restoreDump(dump *dataprovider.BackupData) error {
|
|||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -135,7 +135,7 @@ func newMockOsFs(err, statErr error, atomicUpload bool, connectionID, rootDir st
|
|||
}
|
||||
|
||||
func TestRemoveNonexistentQuotaScan(t *testing.T) {
|
||||
assert.False(t, common.QuotaScans.RemoveUserQuotaScan("username"))
|
||||
assert.False(t, dataprovider.QuotaScans.RemoveUserQuotaScan("username"))
|
||||
}
|
||||
|
||||
func TestGetOSOpenFlags(t *testing.T) {
|
||||
|
|
|
@ -4403,14 +4403,14 @@ func TestQuotaScan(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMultipleQuotaScans(t *testing.T) {
|
||||
res := common.QuotaScans.AddUserQuotaScan(defaultUsername)
|
||||
res := dataprovider.QuotaScans.AddUserQuotaScan(defaultUsername)
|
||||
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.True(t, common.QuotaScans.RemoveUserQuotaScan(defaultUsername))
|
||||
activeScans := common.QuotaScans.GetUsersQuotaScans()
|
||||
assert.True(t, dataprovider.QuotaScans.RemoveUserQuotaScan(defaultUsername))
|
||||
activeScans := dataprovider.QuotaScans.GetUsersQuotaScans()
|
||||
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) {
|
||||
|
@ -6762,15 +6762,15 @@ func TestVirtualFolderQuotaScan(t *testing.T) {
|
|||
|
||||
func TestVFolderMultipleQuotaScan(t *testing.T) {
|
||||
folderName := "folder_name"
|
||||
res := common.QuotaScans.AddVFolderQuotaScan(folderName)
|
||||
res := dataprovider.QuotaScans.AddVFolderQuotaScan(folderName)
|
||||
assert.True(t, res)
|
||||
res = common.QuotaScans.AddVFolderQuotaScan(folderName)
|
||||
res = dataprovider.QuotaScans.AddVFolderQuotaScan(folderName)
|
||||
assert.False(t, res)
|
||||
res = common.QuotaScans.RemoveVFolderQuotaScan(folderName)
|
||||
res = dataprovider.QuotaScans.RemoveVFolderQuotaScan(folderName)
|
||||
assert.True(t, res)
|
||||
activeScans := common.QuotaScans.GetVFoldersQuotaScans()
|
||||
activeScans := dataprovider.QuotaScans.GetVFoldersQuotaScans()
|
||||
assert.Len(t, activeScans, 0)
|
||||
res = common.QuotaScans.RemoveVFolderQuotaScan(folderName)
|
||||
res = dataprovider.QuotaScans.RemoveVFolderQuotaScan(folderName)
|
||||
assert.False(t, res)
|
||||
}
|
||||
|
||||
|
|
|
@ -231,12 +231,7 @@
|
|||
"create_default_admin": false,
|
||||
"naming_rules": 1,
|
||||
"is_shared": 0,
|
||||
"backups_path": "backups",
|
||||
"auto_backup": {
|
||||
"enabled": true,
|
||||
"hour": "0",
|
||||
"day_of_week": "*"
|
||||
}
|
||||
"backups_path": "backups"
|
||||
},
|
||||
"httpd": {
|
||||
"bindings": [
|
||||
|
|
|
@ -22,7 +22,7 @@ const (
|
|||
// EmailContentType defines the support content types for email body
|
||||
type EmailContentType int
|
||||
|
||||
// Supporte email body content type
|
||||
// Supported email body content type
|
||||
const (
|
||||
EmailContentTypeTextPlain EmailContentType = iota
|
||||
EmailContentTypeTextHTML
|
||||
|
@ -169,7 +169,7 @@ func RenderPasswordResetTemplate(buf *bytes.Buffer, data any) error {
|
|||
}
|
||||
|
||||
// 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 {
|
||||
return errors.New("smtp: not configured")
|
||||
}
|
||||
|
@ -184,7 +184,7 @@ func SendEmail(to, subject, body string, contentType EmailContentType) error {
|
|||
} else {
|
||||
email.SetFrom(smtpServer.Username)
|
||||
}
|
||||
email.AddTo(to).SetSubject(subject)
|
||||
email.AddTo(to...).SetSubject(subject)
|
||||
switch contentType {
|
||||
case EmailContentTypeTextPlain:
|
||||
email.SetBody(mail.TextPlain, body)
|
||||
|
|
2
static/vendor/moment/js/moment.min.js
vendored
2
static/vendor/moment/js/moment.min.js
vendored
File diff suppressed because one or more lines are too long
|
@ -74,6 +74,22 @@
|
|||
</li>
|
||||
{{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"}}
|
||||
<li class="nav-item {{if eq .CurrentURL .ConnectionsURL}}active{{end}}">
|
||||
<a class="nav-link" href="{{.ConnectionsURL}}">
|
||||
|
@ -142,6 +158,7 @@
|
|||
|
||||
<!-- Topbar Navbar -->
|
||||
<ul class="navbar-nav ml-auto">
|
||||
{{block "additionalnavitems" .}}{{end}}
|
||||
|
||||
<!-- Nav Item - User Information -->
|
||||
<li class="nav-item dropdown no-arrow">
|
||||
|
|
511
templates/webadmin/eventaction.html
Normal file
511
templates/webadmin/eventaction.html
Normal 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">×</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}}
|
206
templates/webadmin/eventactions.html
Normal file
206
templates/webadmin/eventactions.html
Normal 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">×</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}}
|
541
templates/webadmin/eventrule.html
Normal file
541
templates/webadmin/eventrule.html
Normal 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}}
|
206
templates/webadmin/eventrules.html
Normal file
206
templates/webadmin/eventrules.html
Normal 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">×</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}}
|
|
@ -15,10 +15,6 @@
|
|||
<div id="errorTxt" class="card-body text-form-error"></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-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">View and manage groups</h6>
|
||||
|
|
|
@ -78,154 +78,154 @@ CzgWkxiz7XE4lgUwX44FCXZM3+JeUbI=
|
|||
-----END EC PRIVATE KEY-----`
|
||||
caCRT = `-----BEGIN CERTIFICATE-----
|
||||
MIIE5jCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0
|
||||
QXV0aDAeFw0yMTAxMDIyMTIwNTVaFw0yMjA3MDIyMTMwNTJaMBMxETAPBgNVBAMT
|
||||
CENlcnRBdXRoMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4Tiho5xW
|
||||
AC15JRkMwfp3/TJwI2As7MY5dele5cmdr5bHAE+sRKqC+Ti88OJWCV5saoyax/1S
|
||||
CjxJlQMZMl169P1QYJskKjdG2sdv6RLWLMgwSNRRjxp/Bw9dHdiEb9MjLgu28Jro
|
||||
9peQkHcRHeMf5hM9WvlIJGrdzbC4hUehmqggcqgARainBkYjf0SwuWxHeu4nMqkp
|
||||
Ak5tcSTLCjHfEFHZ9Te0TIPG5YkWocQKyeLgu4lvuU+DD2W2lym+YVUtRMGs1Env
|
||||
k7p+N0DcGU26qfzZ2sF5ZXkqm7dBsGQB9pIxwc2Q8T1dCIyP9OQCKVILdc5aVFf1
|
||||
cryQFHYzYNNZXFlIBims5VV5Mgfp8ESHQSue+v6n6ykecLEyKt1F1Y/MWY/nWUSI
|
||||
8zdq83jdBAZVjo9MSthxVn57/06s/hQca65IpcTZV2gX0a+eRlAVqaRbAhL3LaZe
|
||||
bYsW3WHKoUOftwemuep3nL51TzlXZVL7Oz/ClGaEOsnGG9KFO6jh+W768qC0zLQI
|
||||
CdE7v2Zex98sZteHCg9fGJHIaYoF0aJG5P3WI5oZf2fy7UIYN9ADLFZiorCXAZEh
|
||||
CSU6mDoRViZ4RGR9GZxbDZ9KYn7O8M/KCR72bkQg73TlMsk1zSXEw0MKLUjtsw6c
|
||||
rZ0Jt8t3sRatHO3JrYHALMt9vZfyNCZp0IsCAwEAAaNFMEMwDgYDVR0PAQH/BAQD
|
||||
AgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFO1yCNAGr/zQTJIi8lw3
|
||||
w5OiuBvMMA0GCSqGSIb3DQEBCwUAA4ICAQA6gCNuM7r8mnx674dm31GxBjQy5ZwB
|
||||
7CxDzYEvL/oiZ3Tv3HlPfN2LAAsJUfGnghh9DOytenL2CTZWjl/emP5eijzmlP+9
|
||||
zva5I6CIMCf/eDDVsRdO244t0o4uG7+At0IgSDM3bpVaVb4RHZNjEziYChsEYY8d
|
||||
HK6iwuRSvFniV6yhR/Vj1Ymi9yZ5xclqseLXiQnUB0PkfIk23+7s42cXB16653fH
|
||||
O/FsPyKBLiKJArizLYQc12aP3QOrYoYD9+fAzIIzew7A5C0aanZCGzkuFpO6TRlD
|
||||
Tb7ry9Gf0DfPpCgxraH8tOcmnqp/ka3hjqo/SRnnTk0IFrmmLdarJvjD46rKwBo4
|
||||
MjyAIR1mQ5j8GTlSFBmSgETOQ/EYvO3FPLmra1Fh7L+DvaVzTpqI9fG3TuyyY+Ri
|
||||
Fby4ycTOGSZOe5Fh8lqkX5Y47mCUJ3zHzOA1vUJy2eTlMRGpu47Eb1++Vm6EzPUP
|
||||
2EF5aD+zwcssh+atZvQbwxpgVqVcyLt91RSkKkmZQslh0rnlTb68yxvUnD3zw7So
|
||||
o6TAf9UvwVMEvdLT9NnFd6hwi2jcNte/h538GJwXeBb8EkfpqLKpTKyicnOdkamZ
|
||||
7E9zY8SHNRYMwB9coQ/W8NvufbCgkvOoLyMXk5edbXofXl3PhNGOlraWbghBnzf5
|
||||
r3rwjFsQOoZotA==
|
||||
QXV0aDAeFw0yMjA3MDQxNTQzMTFaFw0yNDAxMDQxNTUzMDhaMBMxETAPBgNVBAMT
|
||||
CENlcnRBdXRoMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4eyDJkmW
|
||||
D4OVYo7ddgiZkd6QQdPyLcsa31Wc9jdR2/peEabyNT8jSWteS6ouY84GRlnhfFeZ
|
||||
mpXgbaUJu/Z8Y/8riPxwL8XF4vCScQDMywpQnVUd6E9x2/+/uaD4p/BBswgKqKPe
|
||||
uDcHZn7MkD4QlquUhMElDrBUi1Dv/AVHnQ6iP4vd5Jlv0F+40jdq/8Wa7yhW7Pu5
|
||||
iNvPwCk8HjENBKVur/re+Acif8A2TlbCsuOnVduSQNmnWH+iZmB9upyBZtUszGS0
|
||||
JhUwtSnwUX/JapF70Pwte/PV3RK8cJ5FjuAPNeTyJvSuMTELFSAyCeiNynFGgyhW
|
||||
cqbEiPu6BURLculyVkmh4dOrhTrYZv/n3UJAhyxkdYrbh3INHmTa4izvclcuwoEo
|
||||
lFlJp3l77D0lIi+pbtcBV6ys7reyuxUAkBNwnpt2pWfCQoi4QYKcNbHm47c2phOb
|
||||
QSojQ8SsNU5bnlY2MDzkKo5DPav/i4d0HpndphUpx4f8hA0KylLevDRkMz9TAH7H
|
||||
uDssn0CxFOGHiveEAGGbn+doHjNWM339x/cdLbK0vuieDKby8YYcBY1JML57Dl9f
|
||||
rs52ySnDZbMqOb9zF66mQpC2FZoAj713xSkDSnSCUekrqgck1EA1ifxAviHt+p26
|
||||
JwaEDL7Lk01EEdYN4csSd1fezbCqTrG8ffUCAwEAAaNFMEMwDgYDVR0PAQH/BAQD
|
||||
AgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFPirPBPO01zUuf7xC+ds
|
||||
bOOY5QvAMA0GCSqGSIb3DQEBCwUAA4ICAQBUYa+ydfTPKjTN4lXyEZgchZQ+juny
|
||||
aMy1xosLz6Evj0us2Bwczmy6X2Zvaw/KteFlgKaU1Ex2UkU7FfAlaH0HtwTLFMVM
|
||||
p9nB7ZzStvg0n8zFM29SEkOFwZ9FRonxx4sY3FdvI4QvAWyDyqgOl8+Eedg0kC4+
|
||||
M7hxarTFmZZ7POZl8Hio592yx3asMmSCcmb7oUCKVI98qsf9fuL+LIZSpn4fE7av
|
||||
AiNBcOqCZ10CRnl4VSgAW2LH4oqROYdUv+me1u1YRwh7fCF/R7VjOLuaDzv0mp/g
|
||||
hzG9U+Yso3WV4b28MsctwUmGTK8Zc5QaANKgmI3ulkta37wN5KjrUuescHC7MqZg
|
||||
vN9n60801be1EoUL83KUx57Bix95YZR02Zge0gYdYTb+E2bwaZ4GMlf7cs6qmC6A
|
||||
ZPLR7Tffw2J4dPTcfEx3rPZ91s3MkAdPzYYGdGlbKp8RCFnezZ7rw2z57rnT0zDr
|
||||
LuL3Q6ADBfothoos/EBIC5ekXb9czp8gig+nJXLC6jlqcQpCLrV88oS3+8zACmx1
|
||||
d6tje9uuAqPgiQGddKZj4b4BlHmAMXq0PufQsZVoyzboTewZiLVCtTR9/iF7Cepg
|
||||
6EVv57p61pFhPu8lNRAi0aH/po9yt+7435FGpn2kan6k9aDIVdaqeuxxITwsqJ4R
|
||||
WwSa13hh6yjoDQ==
|
||||
-----END CERTIFICATE-----`
|
||||
caCRL = `-----BEGIN X509 CRL-----
|
||||
MIICpzCBkAIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0QXV0aBcN
|
||||
MjEwMTAyMjEzNDA1WhcNMjMwMTAyMjEzNDA1WjAkMCICEQC+l04DbHWMyC3fG09k
|
||||
VXf+Fw0yMTAxMDIyMTM0MDVaoCMwITAfBgNVHSMEGDAWgBTtcgjQBq/80EySIvJc
|
||||
N8OTorgbzDANBgkqhkiG9w0BAQsFAAOCAgEAEJ7z+uNc8sqtxlOhSdTGDzX/xput
|
||||
E857kFQkSlMnU2whQ8c+XpYrBLA5vIZJNSSwohTpM4+zVBX/bJpmu3wqqaArRO9/
|
||||
YcW5mQk9Anvb4WjQW1cHmtNapMTzoC9AiYt/OWPfy+P6JCgCr4Hy6LgQyIRL6bM9
|
||||
VYTalolOm1qa4Y5cIeT7iHq/91mfaqo8/6MYRjLl8DOTROpmw8OS9bCXkzGKdCat
|
||||
AbAzwkQUSauyoCQ10rpX+Y64w9ng3g4Dr20aCqPf5osaqplEJ2HTK8ljDTidlslv
|
||||
9anQj8ax3Su89vI8+hK+YbfVQwrThabgdSjQsn+veyx8GlP8WwHLAQ379KjZjWg+
|
||||
OlOSwBeU1vTdP0QcB8X5C2gVujAyuQekbaV86xzIBOj7vZdfHZ6ee30TZ2FKiMyg
|
||||
7/N2OqW0w77ChsjB4MSHJCfuTgIeg62GzuZXLM+Q2Z9LBdtm4Byg+sm/P52adOEg
|
||||
gVb2Zf4KSvsAmA0PIBlu449/QXUFcMxzLFy7mwTeZj2B4Ln0Hm0szV9f9R8MwMtB
|
||||
SyLYxVH+mgqaR6Jkk22Q/yYyLPaELfafX5gp/AIXG8n0zxfVaTvK3auSgb1Q6ZLS
|
||||
5QH9dSIsmZHlPq7GoSXmKpMdjUL8eaky/IMteioyXgsBiATzl5L2dsw6MTX3MDF0
|
||||
QbDK+MzhmbKfDxs=
|
||||
MjIwNzA0MTU1MzU4WhcNMjQwNzAzMTU1MzU4WjAkMCICEQDZo5Q3lhxFuDUsxGNm
|
||||
794YFw0yMjA3MDQxNTUzNThaoCMwITAfBgNVHSMEGDAWgBT4qzwTztNc1Ln+8Qvn
|
||||
bGzjmOULwDANBgkqhkiG9w0BAQsFAAOCAgEA1lK6g8qmhyY6myx8342dDuaauY03
|
||||
0iojkxpasuYcytK6XRm96YqjZK9EETxsHHViVU0vCXES60D6wJ9gw4fTWn3WxEdx
|
||||
nIwbGyjUGHh2y+R3uQsfvwxsdYvDsTLAnOLwOo68dAHWmMDZRmgTuGNoYFxVQRGR
|
||||
Cn90ZR7LPLpCScclWM8FE/W1B90x3ZE8EhJiCI/WyyTh3EgshmB7A5GoDrFZfmvR
|
||||
dzoTKO+F9p2XjtmgfiBE3czWQysfATmbutZUbG/ZRb89u+ZEUyPoC94mg8fhNWoX
|
||||
1d5G9QAkZFHp957/5QHLq9OHNfnWXoohhebjF4VWqZH7w+RtLc8t0PIog2lX4t1o
|
||||
5N/xFk9akvuoyNGg/fYuJBmN162Q0MdeYfYKDGWdXxf6fpHxVr5v2JrIx6gOwubb
|
||||
cIKP22ZBv/PYOeFsAZ755lTl4OTFUjU5ZJEPD6pUc1daaIqfxsxu8gDZP92FZjsB
|
||||
zaalMbh30n2OhagSMBzSLg5rE6WmBzlQX0ZN8YrW4l2Vq6twnnFHY+UyblRZS+d4
|
||||
oHBaoOaxPEkLxNZ8ulzJS4B6c4D1CXOaBEf++snVzRRUOEdX3x7TvkkrLvIsm06R
|
||||
ux0L1zJb9LbZ/1rhuv70z/kIlD55sqYuRqu3RpgTgZuTERU//rYIqWd03Y5Qon8i
|
||||
VoC6Yp9DPldQJrk=
|
||||
-----END X509 CRL-----`
|
||||
client1Crt = `-----BEGIN CERTIFICATE-----
|
||||
MIIEITCCAgmgAwIBAgIRAIppZHoj1hM80D7WzTEKLuAwDQYJKoZIhvcNAQELBQAw
|
||||
EzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjEwMTAyMjEyMzEwWhcNMjIwNzAyMjEz
|
||||
MDUxWjASMRAwDgYDVQQDEwdjbGllbnQxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
|
||||
MIIBCgKCAQEAoKbYY9MdF2kF/nhBESIiZTdVYtA8XL9xrIZyDj9EnCiTxHiVbJtH
|
||||
XVwszqSl5TRrotPmnmAQcX3r8OCk+z+RQZ0QQj257P3kG6q4rNnOcWCS5xEd20jP
|
||||
yhQ3m+hMGfZsotNTQze1ochuQgLUN6IPyPxZkH22ia3jX4iu1eo/QxeLYHj1UHw4
|
||||
3Cii9yE+j5kPUC21xmnrGKdUrB55NYLXHx6yTIqYR5znSOVB8oJi18/hwdZmH859
|
||||
DHhm0Hx1HrS+jbjI3+CMorZJ3WUyNf+CkiVLD3xYutPbxzEpwiqkG/XYzLH0habT
|
||||
cDcILo18n+o3jvem2KWBrDhyairjIDscwQIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC
|
||||
A7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBSJ5GIv
|
||||
zIrE4ZSQt2+CGblKTDswizAfBgNVHSMEGDAWgBTtcgjQBq/80EySIvJcN8OTorgb
|
||||
zDANBgkqhkiG9w0BAQsFAAOCAgEALh4f5GhvNYNou0Ab04iQBbLEdOu2RlbK1B5n
|
||||
K9P/umYenBHMY/z6HT3+6tpcHsDuqE8UVdq3f3Gh4S2Gu9m8PRitT+cJ3gdo9Plm
|
||||
3rD4ufn/s6rGg3ppydXcedm17492tbccUDWOBZw3IO/ASVq13WPgT0/Kev7cPq0k
|
||||
sSdSNhVeXqx8Myc2/d+8GYyzbul2Kpfa7h9i24sK49E9ftnSmsIvngONo08eT1T0
|
||||
3wAOyK2981LIsHaAWcneShKFLDB6LeXIT9oitOYhiykhFlBZ4M1GNlSNfhQ8IIQP
|
||||
xbqMNXCLkW4/BtLhGEEcg0QVso6Kudl9rzgTfQknrdF7pHp6rS46wYUjoSyIY6dl
|
||||
oLmnoAVJX36J3QPWelePI9e07X2wrTfiZWewwgw3KNRWjd6/zfPLe7GoqXnK1S2z
|
||||
PT8qMfCaTwKTtUkzXuTFvQ8bAo2My/mS8FOcpkt2oQWeOsADHAUX7fz5BCoa2DL3
|
||||
k/7Mh4gVT+JYZEoTwCFuYHgMWFWe98naqHi9lB4yR981p1QgXgxO7qBeipagKY1F
|
||||
LlH1iwXUqZ3MZnkNA+4e1Fglsw3sa/rC+L98HnznJ/YbTfQbCP6aQ1qcOymrjMud
|
||||
7MrFwqZjtd/SK4Qx1VpK6jGEAtPgWBTUS3p9ayg6lqjMBjsmySWfvRsDQbq6P5Ct
|
||||
O/e3EH8=
|
||||
MIIEITCCAgmgAwIBAgIRAJla/m/UkZMifNwG+DxFr2MwDQYJKoZIhvcNAQELBQAw
|
||||
EzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjIwNzA0MTU0MzM3WhcNMjQwMTA0MTU1
|
||||
MzA3WjASMRAwDgYDVQQDEwdjbGllbnQxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
|
||||
MIIBCgKCAQEA8xM5v+2QfdzfwnNT5cl+6oEy2fZoI2YG6L6c25rG0pr+yl1IHKdM
|
||||
Zcvn93uat7hlbzxeOLfJRM7+QK1lLaxuppq9p+gT+1x9eG3E4X7e0pdbjrpJGbvN
|
||||
ji0hwDBLDWD8mHNq/SCk9FKtGnfZqrNB5BLw2uIKjJzVGXVlsjN6geBDm2hVjTSm
|
||||
zMr39CfLUdtvMaZhpIPJzbH+sNfp1zKavFIpmwCd77p/z0QAiQ9NaIvzv4PZDDEE
|
||||
MUHzmVAU6bUjD8GToXaMbRiz694SU8aAwvvcdjGexdbHnfSAfLOl2wTPPxvePncR
|
||||
aa656ZeZWxY9pRCItP+v43nm7d4sAyRD4QIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC
|
||||
A7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBQbwDqF
|
||||
aja3ifZHm6mtSeTK9IHc+zAfBgNVHSMEGDAWgBT4qzwTztNc1Ln+8QvnbGzjmOUL
|
||||
wDANBgkqhkiG9w0BAQsFAAOCAgEAprE/zV6u8UIH8g4Jb73wtUD/eIL3iBJ7mNYa
|
||||
lqwCyJrWH7/F9fcovJnF9WO1QPTeHxhoD9rlQK70GitUAeboYw611yNWDS4tDlaL
|
||||
sjpJKykUxBgBR7QSLZCrPtQ3fP2WvlZzLGqB28rASTLphShqTuGp4gJaxGHfbCU7
|
||||
mlV9QYi+InQxOICJJPebXUOwx5wYkFQWJ9qE1AK3QrWPi8QYFznJvHgkNAaMBEmI
|
||||
jAlggOzpveVvy8f4z3QG9o29LIwp7JvtJQs7QXL80FZK98/8US/3gONwTrBz2Imx
|
||||
28ywvwCq7fpMyPgxX4sXtxphCNim+vuHcqDn2CvLS9p/6L6zzqbFNxpmMkJDLrOc
|
||||
YqtHE4TLWIaXpb5JNrYJgNCZyJuYDICVTbivtMacHpSwYtXQ4iuzY2nIr0+4y9i9
|
||||
MNpqv3W47xnvgUQa5vbTbIqo2NSY24A84mF5EyjhaNgNtDlN56+qTQ6HLZNVr6pv
|
||||
eUCCWnY4GkaZUEU1M8/uNtKaZKv1WA7gJxZDQHj8+R110mPtzm1C5jqg7jSjGy9C
|
||||
8PhAwBqIXkVLNayFEtyZZobTxMH5qY1yFkI3sic7S9ZyXt3quY1Q1UT3liRteIm/
|
||||
sZHC5zEoidsHObkTeU44hqZVPkbvrfmgW01xTJjddnMPBH+yqjCCc94yCbW79j/2
|
||||
7LEmxYg=
|
||||
-----END CERTIFICATE-----`
|
||||
client1Key = `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAoKbYY9MdF2kF/nhBESIiZTdVYtA8XL9xrIZyDj9EnCiTxHiV
|
||||
bJtHXVwszqSl5TRrotPmnmAQcX3r8OCk+z+RQZ0QQj257P3kG6q4rNnOcWCS5xEd
|
||||
20jPyhQ3m+hMGfZsotNTQze1ochuQgLUN6IPyPxZkH22ia3jX4iu1eo/QxeLYHj1
|
||||
UHw43Cii9yE+j5kPUC21xmnrGKdUrB55NYLXHx6yTIqYR5znSOVB8oJi18/hwdZm
|
||||
H859DHhm0Hx1HrS+jbjI3+CMorZJ3WUyNf+CkiVLD3xYutPbxzEpwiqkG/XYzLH0
|
||||
habTcDcILo18n+o3jvem2KWBrDhyairjIDscwQIDAQABAoIBAEBSjVFqtbsp0byR
|
||||
aXvyrtLX1Ng7h++at2jca85Ihq//jyqbHTje8zPuNAKI6eNbmb0YGr5OuEa4pD9N
|
||||
ssDmMsKSoG/lRwwcm7h4InkSvBWpFShvMgUaohfHAHzsBYxfnh+TfULsi0y7c2n6
|
||||
t/2OZcOTRkkUDIITnXYiw93ibHHv2Mv2bBDu35kGrcK+c2dN5IL5ZjTjMRpbJTe2
|
||||
44RBJbdTxHBVSgoGBnugF+s2aEma6Ehsj70oyfoVpM6Aed5kGge0A5zA1JO7WCn9
|
||||
Ay/DzlULRXHjJIoRWd2NKvx5n3FNppUc9vJh2plRHalRooZ2+MjSf8HmXlvG2Hpb
|
||||
ScvmWgECgYEA1G+A/2KnxWsr/7uWIJ7ClcGCiNLdk17Pv3DZ3G4qUsU2ITftfIbb
|
||||
tU0Q/b19na1IY8Pjy9ptP7t74/hF5kky97cf1FA8F+nMj/k4+wO8QDI8OJfzVzh9
|
||||
PwielA5vbE+xmvis5Hdp8/od1Yrc/rPSy2TKtPFhvsqXjqoUmOAjDP8CgYEAwZjH
|
||||
9dt1sc2lx/rMxihlWEzQ3JPswKW9/LJAmbRBoSWF9FGNjbX7uhWtXRKJkzb8ZAwa
|
||||
88azluNo2oftbDD/+jw8b2cDgaJHlLAkSD4O1D1RthW7/LKD15qZ/oFsRb13NV85
|
||||
ZNKtwslXGbfVNyGKUVFm7fVA8vBAOUey+LKDFj8CgYEAg8WWstOzVdYguMTXXuyb
|
||||
ruEV42FJaDyLiSirOvxq7GTAKuLSQUg1yMRBIeQEo2X1XU0JZE3dLodRVhuO4EXP
|
||||
g7Dn4X7Th9HSvgvNuIacowWGLWSz4Qp9RjhGhXhezUSx2nseY6le46PmFavJYYSR
|
||||
4PBofMyt4PcyA6Cknh+KHmkCgYEAnTriG7ETE0a7v4DXUpB4TpCEiMCy5Xs2o8Z5
|
||||
ZNva+W+qLVUWq+MDAIyechqeFSvxK6gRM69LJ96lx+XhU58wJiFJzAhT9rK/g+jS
|
||||
bsHH9WOfu0xHkuHA5hgvvV2Le9B2wqgFyva4HJy82qxMxCu/VG/SMqyfBS9OWbb7
|
||||
ibQhdq0CgYAl53LUWZsFSZIth1vux2LVOsI8C3X1oiXDGpnrdlQ+K7z57hq5EsRq
|
||||
GC+INxwXbvKNqp5h0z2MvmKYPDlGVTgw8f8JjM7TkN17ERLcydhdRrMONUryZpo8
|
||||
1xTob+8blyJgfxZUIAKbMbMbIiU0WAF0rfD/eJJwS4htOW/Hfv4TGA==
|
||||
MIIEpAIBAAKCAQEA8xM5v+2QfdzfwnNT5cl+6oEy2fZoI2YG6L6c25rG0pr+yl1I
|
||||
HKdMZcvn93uat7hlbzxeOLfJRM7+QK1lLaxuppq9p+gT+1x9eG3E4X7e0pdbjrpJ
|
||||
GbvNji0hwDBLDWD8mHNq/SCk9FKtGnfZqrNB5BLw2uIKjJzVGXVlsjN6geBDm2hV
|
||||
jTSmzMr39CfLUdtvMaZhpIPJzbH+sNfp1zKavFIpmwCd77p/z0QAiQ9NaIvzv4PZ
|
||||
DDEEMUHzmVAU6bUjD8GToXaMbRiz694SU8aAwvvcdjGexdbHnfSAfLOl2wTPPxve
|
||||
PncRaa656ZeZWxY9pRCItP+v43nm7d4sAyRD4QIDAQABAoIBADE17zcgDWSt1s8z
|
||||
MgUPahZn2beu3x5rhXKRRIhhKWdx4atufy7t39WsFmZQK96OAlsmyZyJ+MFpdqf5
|
||||
csZwZmZsZYEcxw7Yhr5e2sEcQlg4NF0M8ce38cGa+X5DSK6IuBrVIw/kEAE2y7zU
|
||||
Dsk0SV63RvPJV4FoLuxcjB4rtd2c+JBduNUXQYVppz/KhsXN+9CbPbZ7wo1cB5fo
|
||||
Iu/VswvvW6EAxVx39zZcwSGdkss9XUktU8akx7T/pepIH6fwkm7uXSNez6GH9d1I
|
||||
8qOiORk/gAtqPL1TJgConyYheWMM9RbXP/IwL0BV8U4ZVG53S8jx2XpP4OJQ+k35
|
||||
WYvz8JECgYEA+9OywKOG2lMiiUB1qZfmXB80PngNsz+L6xUWkrw58gSqYZIg0xyH
|
||||
Sfr7HBo0yn/PB0oMMWPpNfYvG8/kSMIWiVlsYz9fdsUuqIvN+Kh9VF6o2wn+gnJk
|
||||
sBE3KVMofcgwgLE6eMVv2MSQlBoXhGPNlCBHS1gorQdYE82dxDPBBzsCgYEA9xpm
|
||||
c3C9LxiVbw9ZZ5D2C+vzwIG2+ZeDwKSizM1436MAnzNQgQTMzQ20uFGNBD562VjI
|
||||
rHFlZYr3KCtSIw5gvCSuox0YB64Yq/WAtGZtH9JyKRz4h4juq6iM4FT7nUwM4DF9
|
||||
3CUiDS8DGoqvCNpY50GvzSR5QVT1DKTZsMunh5MCgYEAyIWMq7pK0iQqtvG9/3o1
|
||||
8xrhxfBgsF+kcV+MZvE8jstKRIFQY+oujCkutPTlHm3hE2PSC64L8G0Em/fRRmJO
|
||||
AbZUCT9YK8HdYlZYf2zix0DM4gW2RHcEV/KNYvmVn3q9rGvzLGHCqu/yVAvmuAOk
|
||||
mhON0Z/0W7siVjp/KtEvHisCgYA/cfTaMRkyDXLY6C0BbXPvTa7xP5z2atO2U89F
|
||||
HICrkxOmzKsf5VacU6eSJ8Y4T76FLcmglSD+uHaLRsw5Ggj2Zci9MswntKi7Bjb8
|
||||
msvr/sG3EqwxSJRXWNiLBObx1UP9EFgLfTFIB0kZuIAGmuF2xyPXXUUQ5Dpi+7S1
|
||||
MyUZpwKBgQDg+AIPvk41vQ4Cz2CKrQX5/uJSW4bOhgP1yk7ruIH4Djkag3ZzTnHM
|
||||
zA9/pLzRfz1ENc5I/WaYSh92eKw3j6tUtMJlE2AbfCpgOQtRUNs3IBmzCWrY8J01
|
||||
W/8bwB+KhfFxNYwvszYsvvOq51NgahYQkgThVm38UixB3PFpEf+NiQ==
|
||||
-----END RSA PRIVATE KEY-----`
|
||||
// client 2 crt is revoked
|
||||
client2Crt = `-----BEGIN CERTIFICATE-----
|
||||
MIIEITCCAgmgAwIBAgIRAL6XTgNsdYzILd8bT2RVd/4wDQYJKoZIhvcNAQELBQAw
|
||||
EzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjEwMTAyMjEyMzIwWhcNMjIwNzAyMjEz
|
||||
MDUxWjASMRAwDgYDVQQDEwdjbGllbnQyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
|
||||
MIIBCgKCAQEA6xjW5KQR3/OFQtV5M75WINqQ4AzXSu6DhSz/yumaaQZP/UxY+6hi
|
||||
jcrFzGo9MMie/Sza8DhkXOFAl2BelUubrOeB2cl+/Gr8OCyRi2Gv6j3zCsuN/4jQ
|
||||
tNaoez/IbkDvI3l/ZpzBtnuNY2RiemGgHuORXHRVf3qVlsw+npBIRW5rM2HkO/xG
|
||||
oZjeBErWVu390Lyn+Gvk2TqQDnkutWnxUC60/zPlHhXZ4BwaFAekbSnjsSDB1YFM
|
||||
s8HwW4oBryoxdj3/+/qLrBHt75IdLw3T7/V1UDJQM3EvSQOr12w4egpldhtsC871
|
||||
nnBQZeY6qA5feffIwwg/6lJm70o6S6OX6wIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC
|
||||
A7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBTB84v5
|
||||
t9HqhLhMODbn6oYkEQt3KzAfBgNVHSMEGDAWgBTtcgjQBq/80EySIvJcN8OTorgb
|
||||
zDANBgkqhkiG9w0BAQsFAAOCAgEALGtBCve5k8tToL3oLuXp/oSik6ovIB/zq4I/
|
||||
4zNMYPU31+ZWz6aahysgx1JL1yqTa3Qm8o2tu52MbnV10dM7CIw7c/cYa+c+OPcG
|
||||
5LF97kp13X+r2axy+CmwM86b4ILaDGs2Qyai6VB6k7oFUve+av5o7aUrNFpqGCJz
|
||||
HWdtHZSVA3JMATzy0TfWanwkzreqfdw7qH0yZ9bDURlBKAVWrqnCstva9jRuv+AI
|
||||
eqxr/4Ro986TFjJdoAP3Vr16CPg7/B6GA/KmsBWJrpeJdPWq4i2gpLKvYZoy89qD
|
||||
mUZf34RbzcCtV4NvV1DadGnt4us0nvLrvS5rL2+2uWD09kZYq9RbLkvgzF/cY0fz
|
||||
i7I1bi5XQ+alWe0uAk5ZZL/D+GTRYUX1AWwCqwJxmHrMxcskMyO9pXvLyuSWRDLo
|
||||
YNBrbX9nLcfJzVCp+X+9sntTHjs4l6Cw+fLepJIgtgqdCHtbhTiv68vSM6cgb4br
|
||||
6n2xrXRKuioiWFOrTSRr+oalZh8dGJ/xvwY8IbWknZAvml9mf1VvfE7Ma5P777QM
|
||||
fsbYVTq0Y3R/5hIWsC3HA5z6MIM8L1oRe/YyhP3CTmrCHkVKyDOosGXpGz+JVcyo
|
||||
cfYkY5A3yFKB2HaCwZSfwFmRhxkrYWGEbHv3Cd9YkZs1J3hNhGFZyVMC9Uh0S85a
|
||||
6zdDidU=
|
||||
MIIEITCCAgmgAwIBAgIRANmjlDeWHEW4NSzEY2bv3hgwDQYJKoZIhvcNAQELBQAw
|
||||
EzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjIwNzA0MTU0MzUxWhcNMjQwMTA0MTU1
|
||||
MzA3WjASMRAwDgYDVQQDEwdjbGllbnQyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
|
||||
MIIBCgKCAQEAzNl7q7yS8MSaQs6zRbuqrsUuwEJ5ZH85vf7zHZKgOW3zNniXLOmH
|
||||
JdtQ3jKZQ1BCIsJFvez2GxGIMWbXaSPw4bL0J3vl5oItChsjGg34IvqcDxWuIk2a
|
||||
muRdMh7r1ryVs2ir2cQ5YHzI59BEpUWKQg3bD4yragdkb6BRc7lVgzCbrM1Eq758
|
||||
HHbaLwlsfpqOvheaum4IG113CeD/HHrw42W6g/qQWL+FHlYqV3plHZ8Bj+bhcZI5
|
||||
jdU4paGEzeY0a0NlnyH4gXGPjLKvPKFZHy4D6RiRlLHvHeiRyDtTu4wFkAiXxzGs
|
||||
E4UBbykmYUB85zgwpjaktOaoe36IM1T8CQIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC
|
||||
A7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBRdYIEk
|
||||
gxh+vTaMpAbqaPGRKGGBpTAfBgNVHSMEGDAWgBT4qzwTztNc1Ln+8QvnbGzjmOUL
|
||||
wDANBgkqhkiG9w0BAQsFAAOCAgEABSR/PbPfiNZ6FOrt91/I0g6LviwICDcuXhfr
|
||||
re4UsWp1kxXeS3CB2G71qXv3hswN8phG2hdsij0/FBEGUTLS3FTCmLmqmcVqPj3/
|
||||
677PMFDoACBKgT5iIwpnNvdD+4ROM8JFjUwy7aTWx85a5yoPFGnB+ORMfLCYjr2S
|
||||
D02KFvKuSXWCjXphqJ41cFGne4oeh/JMkN0RNArm7wTT8yWCGgO1k4OON8dphuTV
|
||||
48Wm6I9UBSWuLk1vcIlgb/8YWVwy9rBNmjOBDGuroL6PSmfZD+e9Etii0X2znZ+t
|
||||
qDpXJB7V5U0DbsBCtGM/dHaFz/LCoBYX9z6th1iPUHksUTM3RzN9L24r9/28dY/a
|
||||
shBpn5rK3ui/2mPBpO26wX14Kl/DUkdKUV9dJllSlmwo8Z0RluY9S4xnCrna/ODH
|
||||
FbhWmlTSs+odCZl6Lc0nuw+WQ2HnlTVJYBSFAGfsGQQ3pzk4DC5VynnxY0UniUgD
|
||||
WYPR8JEYa+BpH3rIQ9jmnOKWLtyc7lFPB9ab63pQBBiwRvWo+tZ2vybqjeHPuu5N
|
||||
BuKvvtu3RKKdSCnIo5Rs5zw4JYCjvlx/NVk9jtpa1lIHYHilvBmCcRX5DkE/yH/x
|
||||
IjEKhCOQpGR6D5Kkca9xNL7zNcat3bzLn+d7Wo4m09uWi9ifPdchxed0w5d9ihx1
|
||||
enqNrFI=
|
||||
-----END CERTIFICATE-----`
|
||||
client2Key = `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEA6xjW5KQR3/OFQtV5M75WINqQ4AzXSu6DhSz/yumaaQZP/UxY
|
||||
+6hijcrFzGo9MMie/Sza8DhkXOFAl2BelUubrOeB2cl+/Gr8OCyRi2Gv6j3zCsuN
|
||||
/4jQtNaoez/IbkDvI3l/ZpzBtnuNY2RiemGgHuORXHRVf3qVlsw+npBIRW5rM2Hk
|
||||
O/xGoZjeBErWVu390Lyn+Gvk2TqQDnkutWnxUC60/zPlHhXZ4BwaFAekbSnjsSDB
|
||||
1YFMs8HwW4oBryoxdj3/+/qLrBHt75IdLw3T7/V1UDJQM3EvSQOr12w4egpldhts
|
||||
C871nnBQZeY6qA5feffIwwg/6lJm70o6S6OX6wIDAQABAoIBAFatstVb1KdQXsq0
|
||||
cFpui8zTKOUiduJOrDkWzTygAmlEhYtrccdfXu7OWz0x0lvBLDVGK3a0I/TGrAzj
|
||||
4BuFY+FM/egxTVt9in6fmA3et4BS1OAfCryzUdfK6RV//8L+t+zJZ/qKQzWnugpy
|
||||
QYjDo8ifuMFwtvEoXizaIyBNLAhEp9hnrv+Tyi2O2gahPvCHsD48zkyZRCHYRstD
|
||||
NH5cIrwz9/RJgPO1KI+QsJE7Nh7stR0sbr+5TPU4fnsL2mNhMUF2TJrwIPrc1yp+
|
||||
YIUjdnh3SO88j4TQT3CIrWi8i4pOy6N0dcVn3gpCRGaqAKyS2ZYUj+yVtLO4KwxZ
|
||||
SZ1lNvECgYEA78BrF7f4ETfWSLcBQ3qxfLs7ibB6IYo2x25685FhZjD+zLXM1AKb
|
||||
FJHEXUm3mUYrFJK6AFEyOQnyGKBOLs3S6oTAswMPbTkkZeD1Y9O6uv0AHASLZnK6
|
||||
pC6ub0eSRF5LUyTQ55Jj8D7QsjXJueO8v+G5ihWhNSN9tB2UA+8NBmkCgYEA+weq
|
||||
cvoeMIEMBQHnNNLy35bwfqrceGyPIRBcUIvzQfY1vk7KW6DYOUzC7u+WUzy/hA52
|
||||
DjXVVhua2eMQ9qqtOav7djcMc2W9RbLowxvno7K5qiCss013MeWk64TCWy+WMp5A
|
||||
AVAtOliC3hMkIKqvR2poqn+IBTh1449agUJQqTMCgYEAu06IHGq1GraV6g9XpGF5
|
||||
wqoAlMzUTdnOfDabRilBf/YtSr+J++ThRcuwLvXFw7CnPZZ4TIEjDJ7xjj3HdxeE
|
||||
fYYjineMmNd40UNUU556F1ZLvJfsVKizmkuCKhwvcMx+asGrmA+tlmds4p3VMS50
|
||||
KzDtpKzLWlmU/p/RINWlRmkCgYBy0pHTn7aZZx2xWKqCDg+L2EXPGqZX6wgZDpu7
|
||||
OBifzlfM4ctL2CmvI/5yPmLbVgkgBWFYpKUdiujsyyEiQvWTUKhn7UwjqKDHtcsk
|
||||
G6p7xS+JswJrzX4885bZJ9Oi1AR2yM3sC9l0O7I4lDbNPmWIXBLeEhGMmcPKv/Kc
|
||||
91Ff4wKBgQCF3ur+Vt0PSU0ucrPVHjCe7tqazm0LJaWbPXL1Aw0pzdM2EcNcW/MA
|
||||
w0kqpr7MgJ94qhXCBcVcfPuFN9fBOadM3UBj1B45Cz3pptoK+ScI8XKno6jvVK/p
|
||||
xr5cb9VBRBtB9aOKVfuRhpatAfS2Pzm2Htae9lFn7slGPUmu2hkjDw==
|
||||
MIIEowIBAAKCAQEAzNl7q7yS8MSaQs6zRbuqrsUuwEJ5ZH85vf7zHZKgOW3zNniX
|
||||
LOmHJdtQ3jKZQ1BCIsJFvez2GxGIMWbXaSPw4bL0J3vl5oItChsjGg34IvqcDxWu
|
||||
Ik2amuRdMh7r1ryVs2ir2cQ5YHzI59BEpUWKQg3bD4yragdkb6BRc7lVgzCbrM1E
|
||||
q758HHbaLwlsfpqOvheaum4IG113CeD/HHrw42W6g/qQWL+FHlYqV3plHZ8Bj+bh
|
||||
cZI5jdU4paGEzeY0a0NlnyH4gXGPjLKvPKFZHy4D6RiRlLHvHeiRyDtTu4wFkAiX
|
||||
xzGsE4UBbykmYUB85zgwpjaktOaoe36IM1T8CQIDAQABAoIBAETHMJK0udFE8VZE
|
||||
+EQNgn0zj0LWDtQDM2vrUc04Ebu2gtZjHr7hmZLIVBqGepbzN4FcIPZnvSnRdRzB
|
||||
HsoaWyIsZ3VqUAJY6q5d9iclUY7M/eDCsripvaML0Y6meyCaKNkX57sx+uG+g+Xx
|
||||
M1saQhVzeX17CYKMANjJxw9HxsJI0aBPyiBbILHMwfRfsJU8Ou72HH1sIQuPdH2H
|
||||
/c9ru8YZAno6oVq1zuC/pCis+h50U9HzTnt3/4NNS6cWG/y2YLztCvm9uGo4MTd/
|
||||
mA9s4cxVhvQW6gCDHgGn6zj661OL/d2rpak1eWizhZvZ8jsIN/sM87b0AJeVT4zH
|
||||
6xA3egECgYEA1nI5EsCetQbFBp7tDovSp3fbitwoQtdtHtLn2u4DfvmbLrgSoq0Z
|
||||
L+9N13xML/l8lzWai2gI69uA3c2+y1O64LkaiSeDqbeBp9b6fKMlmwIVbklEke1w
|
||||
XVTIWOYTTF5/8+tUOlsgme5BhLAWnQ7+SoitzHtl5e1vEYaAGamE2DECgYEA9Is2
|
||||
BbTk2YCqkcsB7D9q95JbY0SZpecvTv0rLR+acz3T8JrAASdmvqdBOlPWc+0ZaEdS
|
||||
PcJaOEw3yxYJ33cR/nLBaR2/Uu5qQebyPALs3B2pjjTFdGvcpeFxO55fowwsfR/e
|
||||
0H+HeiFj5Y4S+kFWT+3FRmJ6GUB828LJYaVhQ1kCgYEA1bdsTdYN1Vfzz89fbZnH
|
||||
zQLUl6UlssfDhm6mhzeh4E+eaocke1+LtIwHxfOocj9v/bp8VObPzU8rNOIxfa3q
|
||||
lr+jRIFO5DtwSfckGEb32W3QMeNvJQe/biRqrr5NCVU8q7kibi4XZZFfVn+vacNh
|
||||
hqKEoz9vpCBnCs5CqFCbhmECgYAG8qWYR+lwnI08Ey58zdh2LDxYd6x94DGh5uOB
|
||||
JrK2r30ECwGFht8Ob6YUyCkBpizgn5YglxMFInU7Webx6GokdpI0MFotOwTd1nfv
|
||||
aI3eOyGEHs+1XRMpy1vyO6+v7DqfW3ZzKgxpVeWGsiCr54tSPgkq1MVvTju96qza
|
||||
D17SEQKBgCKC0GjDjnt/JvujdzHuBt1sWdOtb+B6kQvA09qVmuDF/Dq36jiaHDjg
|
||||
XMf5HU3ThYqYn3bYypZZ8nQ7BXVh4LqGNqG29wR4v6l+dLO6odXnLzfApGD9e+d4
|
||||
2tmlLP54LaN35hQxRjhT8lCN0BkrNF44+bh8frwm/kuxSd8wT2S+
|
||||
-----END RSA PRIVATE KEY-----`
|
||||
testFileName = "test_file_dav.dat"
|
||||
testDLFileName = "test_download_dav.dat"
|
||||
|
|
Loading…
Reference in a new issue