mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-24 16:40:26 +00:00
eventmanager: add support for global star path matching
This introduce a backward incompatible change for filesystem path matching in the Event Manager, now patterns like "*.txt" will no longer match any file with the "txt" suffix, you need to change them to "/**/*.txt". Also change pre-delete behaviour, now if an error is returned the client will get a permission denied error. This is the same as the other pre-* action. Previously it was not possible to deny deletion of a file. Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
2611dd2c98
commit
7fa0959af4
17 changed files with 179 additions and 85 deletions
|
@ -28,9 +28,7 @@ For cloud backends directories are virtual, they are created implicitly when you
|
|||
|
||||
The notification will indicate if an error is detected and so, for example, a partial file is uploaded.
|
||||
|
||||
The `pre-delete` action, if defined, will be called just before files deletion. If the external command completes with a zero exit status or the HTTP notification response code is `200`, SFTPGo will assume that the file was already deleted/moved and so it will not try to remove the file and it will not execute the hook defined for the `delete` action.
|
||||
|
||||
The `pre-download` and `pre-upload` actions, will be called before downloads and uploads. If the external command completes with a zero exit status or the HTTP notification response code is `200`, SFTPGo will allow the operation, otherwise the client will get a permission denied error.
|
||||
The `pre-delete`, `pre-download` and `pre-upload` actions, will be called before deleting, downloading and uploading files. If the external command completes with a zero exit status or the HTTP notification response code is `200`, SFTPGo will allow the operation, otherwise the client will get a permission denied error.
|
||||
|
||||
If the `hook` defines a path to an external program, then this program can read the following environment variables:
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ Actions are executed in a sequential order except for sync actions that are exec
|
|||
|
||||
- `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. :warning: Please note that a failure action isn't executed if the event fails, for example if a download fails the main action is executed. The failure action is executed only if one of the non-failure actions associated to a rule fails.
|
||||
- `Execute sync`, for upload events, you can execute the action(s) 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. For pre-* events at least a sync action is required. If pre-delete sync action(s) completes successfully, SFTPGo will assume that the file was already deleted/moved and so it will not try to remove the file and it will not execute any defined `delete` actions. If pre-upload/download action(s) completes successfully, SFTPGo will allow the operation, otherwise the client will get a permission denied error.
|
||||
- `Execute sync`, for upload events, you can execute the action(s) 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. For pre-* events at least a sync action is required. If pre-delete,pre-upload, pre-download sync action(s) completes successfully, SFTPGo will allow the operation, otherwise the client will get a permission denied error.
|
||||
|
||||
If you are running multiple SFTPGo instances connected to the same data provider, you can choose whether to allow simultaneous execution for scheduled actions.
|
||||
|
||||
|
|
1
go.mod
1
go.mod
|
@ -17,6 +17,7 @@ require (
|
|||
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.6
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.17.0
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.17.7
|
||||
github.com/bmatcuk/doublestar/v4 v4.4.0
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.2.20
|
||||
github.com/coreos/go-oidc/v3 v3.4.0
|
||||
github.com/drakkan/webdav v0.0.0-20221101181759-17ed21f9337b
|
||||
|
|
2
go.sum
2
go.sum
|
@ -304,6 +304,8 @@ github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edY
|
|||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||
github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/bmatcuk/doublestar/v4 v4.4.0 h1:LmAwNwhjEbYtyVLzjcP/XeVw4nhuScHGkF/XWXnvIic=
|
||||
github.com/bmatcuk/doublestar/v4 v4.4.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs=
|
||||
|
|
|
@ -41,8 +41,6 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
errUnconfiguredAction = errors.New("no hook is configured for this action")
|
||||
errNoHook = errors.New("unable to execute action, no hook defined")
|
||||
errUnexpectedHTTResponse = errors.New("unexpected HTTP hook response code")
|
||||
hooksConcurrencyGuard = make(chan struct{}, 150)
|
||||
activeHooks atomic.Int32
|
||||
|
@ -80,24 +78,18 @@ func InitializeActionHandler(handler ActionHandler) {
|
|||
actionHandler = handler
|
||||
}
|
||||
|
||||
func handleUnconfiguredPreAction(operation string) error {
|
||||
// for pre-delete we execute the internal handling on error, so we must return errUnconfiguredAction.
|
||||
// Other pre action will deny the operation on error so if we have no configuration we must return
|
||||
// a nil error
|
||||
if operation == operationPreDelete {
|
||||
return errUnconfiguredAction
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExecutePreAction executes a pre-* action and returns the result
|
||||
func ExecutePreAction(conn *BaseConnection, operation, filePath, virtualPath string, fileSize int64, openFlags int) error {
|
||||
// ExecutePreAction executes a pre-* action and returns the result.
|
||||
// The returned status has the following meaning:
|
||||
// - 0 not executed
|
||||
// - 1 executed using an external hook
|
||||
// - 2 executed using the event manager
|
||||
func ExecutePreAction(conn *BaseConnection, operation, filePath, virtualPath string, fileSize int64, openFlags int) (int, error) {
|
||||
var event *notifier.FsEvent
|
||||
hasNotifiersPlugin := plugin.Handler.HasNotifiers()
|
||||
hasHook := util.Contains(Config.Actions.ExecuteOn, operation)
|
||||
hasRules := eventManager.hasFsRules()
|
||||
if !hasHook && !hasNotifiersPlugin && !hasRules {
|
||||
return handleUnconfiguredPreAction(operation)
|
||||
return 0, nil
|
||||
}
|
||||
event = newActionNotification(&conn.User, operation, filePath, virtualPath, "", "", "",
|
||||
conn.protocol, conn.GetRemoteIP(), conn.ID, fileSize, openFlags, conn.getNotificationStatus(nil))
|
||||
|
@ -124,11 +116,11 @@ func ExecutePreAction(conn *BaseConnection, operation, filePath, virtualPath str
|
|||
}
|
||||
executedSync, err := eventManager.handleFsEvent(params)
|
||||
if executedSync {
|
||||
return err
|
||||
return 2, err
|
||||
}
|
||||
}
|
||||
if !hasHook {
|
||||
return handleUnconfiguredPreAction(operation)
|
||||
return 0, nil
|
||||
}
|
||||
return actionHandler.Handle(event)
|
||||
}
|
||||
|
@ -176,7 +168,8 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua
|
|||
}
|
||||
if hasHook {
|
||||
if util.Contains(Config.Actions.ExecuteSync, operation) {
|
||||
return actionHandler.Handle(notification)
|
||||
_, err := actionHandler.Handle(notification)
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
startNewHook()
|
||||
|
@ -190,7 +183,7 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua
|
|||
|
||||
// ActionHandler handles a notification for a Protocol Action.
|
||||
type ActionHandler interface {
|
||||
Handle(notification *notifier.FsEvent) error
|
||||
Handle(notification *notifier.FsEvent) (int, error)
|
||||
}
|
||||
|
||||
func newActionNotification(
|
||||
|
@ -244,28 +237,30 @@ func newActionNotification(
|
|||
|
||||
type defaultActionHandler struct{}
|
||||
|
||||
func (h *defaultActionHandler) Handle(event *notifier.FsEvent) error {
|
||||
func (h *defaultActionHandler) Handle(event *notifier.FsEvent) (int, error) {
|
||||
if !util.Contains(Config.Actions.ExecuteOn, event.Action) {
|
||||
return errUnconfiguredAction
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if Config.Actions.Hook == "" {
|
||||
logger.Warn(event.Protocol, "", "Unable to send notification, no hook is defined")
|
||||
|
||||
return errNoHook
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(Config.Actions.Hook, "http") {
|
||||
return h.handleHTTP(event)
|
||||
err := h.handleHTTP(event)
|
||||
return 1, err
|
||||
}
|
||||
|
||||
return h.handleCommand(event)
|
||||
err := h.handleCommand(event)
|
||||
return 1, err
|
||||
}
|
||||
|
||||
func (h *defaultActionHandler) handleHTTP(event *notifier.FsEvent) error {
|
||||
u, err := url.Parse(Config.Actions.Hook)
|
||||
if err != nil {
|
||||
logger.Error(event.Protocol, "", "Invalid hook %#v for operation %#v: %v",
|
||||
logger.Error(event.Protocol, "", "Invalid hook %q for operation %q: %v",
|
||||
Config.Actions.Hook, event.Action, err)
|
||||
return err
|
||||
}
|
||||
|
@ -294,7 +289,7 @@ func (h *defaultActionHandler) handleHTTP(event *notifier.FsEvent) error {
|
|||
|
||||
func (h *defaultActionHandler) handleCommand(event *notifier.FsEvent) error {
|
||||
if !filepath.IsAbs(Config.Actions.Hook) {
|
||||
err := fmt.Errorf("invalid notification command %#v", Config.Actions.Hook)
|
||||
err := fmt.Errorf("invalid notification command %q", Config.Actions.Hook)
|
||||
logger.Warn(event.Protocol, "", "unable to execute notification command: %v", err)
|
||||
|
||||
return err
|
||||
|
|
|
@ -136,18 +136,21 @@ func TestActionHTTP(t *testing.T) {
|
|||
}
|
||||
a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", "", ProtocolSFTP, "",
|
||||
xid.New().String(), 123, 0, 1)
|
||||
err := actionHandler.Handle(a)
|
||||
status, err := actionHandler.Handle(a)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, status)
|
||||
|
||||
Config.Actions.Hook = "http://invalid:1234"
|
||||
err = actionHandler.Handle(a)
|
||||
status, err = actionHandler.Handle(a)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 1, status)
|
||||
|
||||
Config.Actions.Hook = fmt.Sprintf("http://%v/404", httpAddr)
|
||||
err = actionHandler.Handle(a)
|
||||
status, err = actionHandler.Handle(a)
|
||||
if assert.Error(t, err) {
|
||||
assert.EqualError(t, err, errUnexpectedHTTResponse.Error())
|
||||
}
|
||||
assert.Equal(t, 1, status)
|
||||
|
||||
Config.Actions = actionsCopy
|
||||
}
|
||||
|
@ -173,8 +176,9 @@ func TestActionCMD(t *testing.T) {
|
|||
sessionID := shortuuid.New()
|
||||
a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", "", ProtocolSFTP, "", sessionID,
|
||||
123, 0, 1)
|
||||
err = actionHandler.Handle(a)
|
||||
status, err := actionHandler.Handle(a)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, status)
|
||||
|
||||
c := NewBaseConnection("id", ProtocolSFTP, "", "", *user)
|
||||
err = ExecuteActionNotification(c, OperationSSHCmd, "path", "vpath", "target", "vtarget", "sha1sum", 0, nil)
|
||||
|
@ -205,29 +209,32 @@ func TestWrongActions(t *testing.T) {
|
|||
|
||||
a := newActionNotification(user, operationUpload, "", "", "", "", "", ProtocolSFTP, "", xid.New().String(),
|
||||
123, 0, 1)
|
||||
err := actionHandler.Handle(a)
|
||||
status, err := actionHandler.Handle(a)
|
||||
assert.Error(t, err, "action with bad command must fail")
|
||||
assert.Equal(t, 1, status)
|
||||
|
||||
a.Action = operationDelete
|
||||
err = actionHandler.Handle(a)
|
||||
assert.EqualError(t, err, errUnconfiguredAction.Error())
|
||||
status, err = actionHandler.Handle(a)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, status)
|
||||
|
||||
Config.Actions.Hook = "http://foo\x7f.com/"
|
||||
a.Action = operationUpload
|
||||
err = actionHandler.Handle(a)
|
||||
status, err = actionHandler.Handle(a)
|
||||
assert.Error(t, err, "action with bad url must fail")
|
||||
assert.Equal(t, 1, status)
|
||||
|
||||
Config.Actions.Hook = ""
|
||||
err = actionHandler.Handle(a)
|
||||
if assert.Error(t, err) {
|
||||
assert.EqualError(t, err, errNoHook.Error())
|
||||
}
|
||||
status, err = actionHandler.Handle(a)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, status)
|
||||
|
||||
Config.Actions.Hook = "relative path"
|
||||
err = actionHandler.Handle(a)
|
||||
status, err = actionHandler.Handle(a)
|
||||
if assert.Error(t, err) {
|
||||
assert.EqualError(t, err, fmt.Sprintf("invalid notification command %#v", Config.Actions.Hook))
|
||||
assert.EqualError(t, err, fmt.Sprintf("invalid notification command %q", Config.Actions.Hook))
|
||||
}
|
||||
assert.Equal(t, 1, status)
|
||||
|
||||
Config.Actions = actionsCopy
|
||||
}
|
||||
|
@ -242,7 +249,7 @@ func TestPreDeleteAction(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
Config.Actions = ProtocolActions{
|
||||
ExecuteOn: []string{operationPreDelete},
|
||||
Hook: hookCmd,
|
||||
Hook: "missing hook",
|
||||
}
|
||||
homeDir := filepath.Join(os.TempDir(), "test_user")
|
||||
err = os.MkdirAll(homeDir, os.ModePerm)
|
||||
|
@ -264,8 +271,12 @@ func TestPreDeleteAction(t *testing.T) {
|
|||
info, err := os.Stat(testfile)
|
||||
assert.NoError(t, err)
|
||||
err = c.RemoveFile(fs, testfile, "testfile", info)
|
||||
assert.NoError(t, err)
|
||||
assert.ErrorIs(t, err, c.GetPermissionDeniedError())
|
||||
assert.FileExists(t, testfile)
|
||||
Config.Actions.Hook = hookCmd
|
||||
err = c.RemoveFile(fs, testfile, "testfile", info)
|
||||
assert.NoError(t, err)
|
||||
assert.NoFileExists(t, testfile)
|
||||
|
||||
os.RemoveAll(homeDir)
|
||||
|
||||
|
@ -289,10 +300,12 @@ func TestUnconfiguredHook(t *testing.T) {
|
|||
assert.True(t, plugin.Handler.HasNotifiers())
|
||||
|
||||
c := NewBaseConnection("id", ProtocolSFTP, "", "", dataprovider.User{})
|
||||
err = ExecutePreAction(c, OperationPreDownload, "", "", 0, 0)
|
||||
status, err := ExecutePreAction(c, OperationPreDownload, "", "", 0, 0)
|
||||
assert.NoError(t, err)
|
||||
err = ExecutePreAction(c, operationPreDelete, "", "", 0, 0)
|
||||
assert.ErrorIs(t, err, errUnconfiguredAction)
|
||||
assert.Equal(t, status, 0)
|
||||
status, err = ExecutePreAction(c, operationPreDelete, "", "", 0, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, status, 0)
|
||||
|
||||
err = ExecuteActionNotification(c, operationDownload, "", "", "", "", "", 0, nil)
|
||||
assert.NoError(t, err)
|
||||
|
@ -308,10 +321,10 @@ type actionHandlerStub struct {
|
|||
called bool
|
||||
}
|
||||
|
||||
func (h *actionHandlerStub) Handle(event *notifier.FsEvent) error {
|
||||
func (h *actionHandlerStub) Handle(event *notifier.FsEvent) (int, error) {
|
||||
h.called = true
|
||||
|
||||
return nil
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
func TestInitializeActionHandler(t *testing.T) {
|
||||
|
@ -322,8 +335,8 @@ func TestInitializeActionHandler(t *testing.T) {
|
|||
InitializeActionHandler(&defaultActionHandler{})
|
||||
})
|
||||
|
||||
err := actionHandler.Handle(¬ifier.FsEvent{})
|
||||
|
||||
status, err := actionHandler.Handle(¬ifier.FsEvent{})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, handler.called)
|
||||
assert.Equal(t, 1, status)
|
||||
}
|
||||
|
|
|
@ -391,11 +391,18 @@ func (c *BaseConnection) RemoveFile(fs vfs.Fs, fsPath, virtualPath string, info
|
|||
}
|
||||
|
||||
size := info.Size()
|
||||
actionErr := ExecutePreAction(c, operationPreDelete, fsPath, virtualPath, size, 0)
|
||||
if actionErr == nil {
|
||||
c.Log(logger.LevelDebug, "remove for path %q handled by pre-delete action", fsPath)
|
||||
} else {
|
||||
if err := fs.Remove(fsPath, false); err != nil {
|
||||
status, err := ExecutePreAction(c, operationPreDelete, fsPath, virtualPath, size, 0)
|
||||
if err != nil {
|
||||
c.Log(logger.LevelDebug, "delete for file %q denied by pre action: %v", virtualPath, err)
|
||||
return c.GetPermissionDeniedError()
|
||||
}
|
||||
updateQuota := true
|
||||
if err := fs.Remove(fsPath, false); err != nil {
|
||||
if status > 0 && fs.IsNotExist(err) {
|
||||
// file removed in the pre-action, if the file was deleted from the EventManager the quota is already updated
|
||||
c.Log(logger.LevelDebug, "file deleted from the hook, status: %d", status)
|
||||
updateQuota = (status == 1)
|
||||
} else {
|
||||
c.Log(logger.LevelError, "failed to remove file/symlink %q: %+v", fsPath, err)
|
||||
return c.GetFsError(fs, err)
|
||||
}
|
||||
|
@ -403,7 +410,7 @@ func (c *BaseConnection) RemoveFile(fs vfs.Fs, fsPath, virtualPath string, info
|
|||
|
||||
logger.CommandLog(removeLogSender, fsPath, "", c.User.Username, "", c.ID, c.protocol, -1, -1, "", "", "", -1,
|
||||
c.localAddr, c.remoteAddr)
|
||||
if info.Mode()&os.ModeSymlink == 0 {
|
||||
if updateQuota && info.Mode()&os.ModeSymlink == 0 {
|
||||
vfolder, err := c.User.GetVirtualFolderForPath(path.Dir(virtualPath))
|
||||
if err == nil {
|
||||
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, -1, -size, false) //nolint:errcheck
|
||||
|
@ -414,9 +421,7 @@ func (c *BaseConnection) RemoveFile(fs vfs.Fs, fsPath, virtualPath string, info
|
|||
dataprovider.UpdateUserQuota(&c.User, -1, -size, false) //nolint:errcheck
|
||||
}
|
||||
}
|
||||
if actionErr != nil {
|
||||
ExecuteActionNotification(c, operationDelete, fsPath, virtualPath, "", "", "", size, nil) //nolint:errcheck
|
||||
}
|
||||
ExecuteActionNotification(c, operationDelete, fsPath, virtualPath, "", "", "", size, nil) //nolint:errcheck
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ import (
|
|||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
"github.com/klauspost/compress/zip"
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/rs/xid"
|
||||
|
@ -285,9 +286,7 @@ func (r *eventRulesContainer) checkFsEventMatch(conditions dataprovider.EventCon
|
|||
return false
|
||||
}
|
||||
if !checkEventConditionPatterns(params.VirtualPath, conditions.Options.FsPaths) {
|
||||
if !checkEventConditionPatterns(params.ObjectName, conditions.Options.FsPaths) {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
if len(conditions.Options.Protocols) > 0 && !util.Contains(conditions.Options.Protocols, params.Protocol) {
|
||||
return false
|
||||
|
@ -966,7 +965,13 @@ func replaceWithReplacer(input string, replacer *strings.Replacer) string {
|
|||
}
|
||||
|
||||
func checkEventConditionPattern(p dataprovider.ConditionPattern, name string) bool {
|
||||
matched, err := path.Match(p.Pattern, name)
|
||||
var matched bool
|
||||
var err error
|
||||
if strings.Contains(p.Pattern, "**") {
|
||||
matched, err = doublestar.Match(p.Pattern, name)
|
||||
} else {
|
||||
matched, err = path.Match(p.Pattern, name)
|
||||
}
|
||||
if err != nil {
|
||||
eventManagerLog(logger.LevelError, "pattern matching error %q, err: %v", p.Pattern, err)
|
||||
return false
|
||||
|
|
|
@ -119,7 +119,7 @@ func TestEventRuleMatch(t *testing.T) {
|
|||
},
|
||||
FsPaths: []dataprovider.ConditionPattern{
|
||||
{
|
||||
Pattern: "*.txt",
|
||||
Pattern: "/**/*.txt",
|
||||
},
|
||||
},
|
||||
Protocols: []string{ProtocolSFTP},
|
||||
|
@ -268,6 +268,40 @@ func TestEventRuleMatch(t *testing.T) {
|
|||
assert.False(t, res)
|
||||
}
|
||||
|
||||
func TestDoubleStarMatching(t *testing.T) {
|
||||
c := dataprovider.ConditionPattern{
|
||||
Pattern: "/mydir/**",
|
||||
}
|
||||
res := checkEventConditionPattern(c, "/mydir")
|
||||
assert.True(t, res)
|
||||
res = checkEventConditionPattern(c, "/mydirname")
|
||||
assert.False(t, res)
|
||||
res = checkEventConditionPattern(c, "/mydir/sub")
|
||||
assert.True(t, res)
|
||||
res = checkEventConditionPattern(c, "/mydir/sub/dir")
|
||||
assert.True(t, res)
|
||||
|
||||
c.Pattern = "/**/*"
|
||||
res = checkEventConditionPattern(c, "/mydir")
|
||||
assert.True(t, res)
|
||||
res = checkEventConditionPattern(c, "/mydirname")
|
||||
assert.True(t, res)
|
||||
res = checkEventConditionPattern(c, "/mydir/sub/dir/file.txt")
|
||||
assert.True(t, res)
|
||||
|
||||
c.Pattern = "/mydir/**/*.txt"
|
||||
res = checkEventConditionPattern(c, "/mydir")
|
||||
assert.False(t, res)
|
||||
res = checkEventConditionPattern(c, "/mydirname/f.txt")
|
||||
assert.False(t, res)
|
||||
res = checkEventConditionPattern(c, "/mydir/sub")
|
||||
assert.False(t, res)
|
||||
res = checkEventConditionPattern(c, "/mydir/sub/dir")
|
||||
assert.False(t, res)
|
||||
res = checkEventConditionPattern(c, "/mydir/sub/dir/a.txt")
|
||||
assert.True(t, res)
|
||||
}
|
||||
|
||||
func TestEventManager(t *testing.T) {
|
||||
startEventScheduler()
|
||||
action := &dataprovider.BaseEventAction{
|
||||
|
|
|
@ -3470,7 +3470,7 @@ func TestEventRule(t *testing.T) {
|
|||
Pattern: "/subdir/*.dat",
|
||||
},
|
||||
{
|
||||
Pattern: "*.txt",
|
||||
Pattern: "/**/*.txt",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -3520,7 +3520,7 @@ func TestEventRule(t *testing.T) {
|
|||
Options: dataprovider.ConditionOptions{
|
||||
FsPaths: []dataprovider.ConditionPattern{
|
||||
{
|
||||
Pattern: "*.dat",
|
||||
Pattern: "/**/*.dat",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -4231,6 +4231,14 @@ func TestEventRulePreDelete(t *testing.T) {
|
|||
Trigger: dataprovider.EventTriggerFsEvent,
|
||||
Conditions: dataprovider.EventConditions{
|
||||
FsEvents: []string{"pre-delete"},
|
||||
Options: dataprovider.ConditionOptions{
|
||||
FsPaths: []dataprovider.ConditionPattern{
|
||||
{
|
||||
Pattern: fmt.Sprintf("/%s/**", movePath),
|
||||
InverseMatch: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Actions: []dataprovider.EventAction{
|
||||
{
|
||||
|
@ -4255,7 +4263,19 @@ func TestEventRulePreDelete(t *testing.T) {
|
|||
}
|
||||
rule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
u := getTestUser()
|
||||
u.QuotaFiles = 1000
|
||||
u.VirtualFolders = []vfs.VirtualFolder{
|
||||
{
|
||||
BaseVirtualFolder: vfs.BaseVirtualFolder{
|
||||
Name: movePath,
|
||||
MappedPath: filepath.Join(os.TempDir(), movePath),
|
||||
},
|
||||
VirtualPath: "/" + movePath,
|
||||
QuotaFiles: 1000,
|
||||
},
|
||||
}
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
conn, client, err := getSftpClient(user)
|
||||
if assert.NoError(t, err) {
|
||||
|
@ -4273,7 +4293,7 @@ func TestEventRulePreDelete(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = client.Remove(path.Join(testDir, testFileName))
|
||||
assert.NoError(t, err)
|
||||
// check
|
||||
// check files
|
||||
_, err = client.Stat(testFileName)
|
||||
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||
_, err = client.Stat(path.Join(testDir, testFileName))
|
||||
|
@ -4282,6 +4302,23 @@ func TestEventRulePreDelete(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
_, err = client.Stat(path.Join("/", movePath, testDir, testFileName))
|
||||
assert.NoError(t, err)
|
||||
// check quota
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, user.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), user.UsedQuotaSize)
|
||||
folder, _, err := httpdtest.GetFolderByName(movePath, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, folder.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(200), folder.UsedQuotaSize)
|
||||
// pre-delete action is not executed in movePath
|
||||
err = client.Remove(path.Join("/", movePath, testFileName))
|
||||
assert.NoError(t, err)
|
||||
// check quota
|
||||
folder, _, err = httpdtest.GetFolderByName(movePath, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, folder.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(100), folder.UsedQuotaSize)
|
||||
}
|
||||
|
||||
_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)
|
||||
|
@ -4294,6 +4331,10 @@ func TestEventRulePreDelete(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: movePath}, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(filepath.Join(os.TempDir(), movePath))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestEventRulePreDownloadUpload(t *testing.T) {
|
||||
|
|
|
@ -342,7 +342,7 @@ func (c *Connection) downloadFile(fs vfs.Fs, fsPath, ftpPath string, offset int6
|
|||
return nil, c.GetErrorForDeniedFile(policy)
|
||||
}
|
||||
|
||||
if err := common.ExecutePreAction(c.BaseConnection, common.OperationPreDownload, fsPath, ftpPath, 0, 0); err != nil {
|
||||
if _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreDownload, fsPath, ftpPath, 0, 0); err != nil {
|
||||
c.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", ftpPath, err)
|
||||
return nil, c.GetPermissionDeniedError()
|
||||
}
|
||||
|
@ -404,7 +404,7 @@ func (c *Connection) handleFTPUploadToNewFile(fs vfs.Fs, flags int, resolvedPath
|
|||
c.Log(logger.LevelInfo, "denying file write due to quota limits")
|
||||
return nil, ftpserver.ErrStorageExceeded
|
||||
}
|
||||
if err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, 0, 0); err != nil {
|
||||
if _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, 0, 0); err != nil {
|
||||
c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
||||
return nil, fmt.Errorf("%w, denied by pre-upload action", ftpserver.ErrFileNameNotAllowed)
|
||||
}
|
||||
|
@ -449,7 +449,7 @@ func (c *Connection) handleFTPUploadToExistingFile(fs vfs.Fs, flags int, resolve
|
|||
c.Log(logger.LevelDebug, "unable to get max write size: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
if err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, fileSize, flags); err != nil {
|
||||
if _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, fileSize, flags); err != nil {
|
||||
c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
||||
return nil, fmt.Errorf("%w, denied by pre-upload action", ftpserver.ErrFileNameNotAllowed)
|
||||
}
|
||||
|
|
|
@ -118,7 +118,7 @@ func (c *Connection) getFileReader(name string, offset int64, method string) (io
|
|||
}
|
||||
|
||||
if method != http.MethodHead {
|
||||
if err := common.ExecutePreAction(c.BaseConnection, common.OperationPreDownload, p, name, 0, 0); err != nil {
|
||||
if _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreDownload, p, name, 0, 0); err != nil {
|
||||
c.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", name, err)
|
||||
return nil, c.GetPermissionDeniedError()
|
||||
}
|
||||
|
@ -193,7 +193,7 @@ func (c *Connection) handleUploadFile(fs vfs.Fs, resolvedPath, filePath, request
|
|||
c.Log(logger.LevelInfo, "denying file write due to quota limits")
|
||||
return nil, common.ErrQuotaExceeded
|
||||
}
|
||||
err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, fileSize, os.O_TRUNC)
|
||||
_, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, fileSize, os.O_TRUNC)
|
||||
if err != nil {
|
||||
c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
||||
return nil, c.GetPermissionDeniedError()
|
||||
|
|
|
@ -93,7 +93,7 @@ func (c *Connection) Fileread(request *sftp.Request) (io.ReaderAt, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if err := common.ExecutePreAction(c.BaseConnection, common.OperationPreDownload, p, request.Filepath, 0, 0); err != nil {
|
||||
if _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreDownload, p, request.Filepath, 0, 0); err != nil {
|
||||
c.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", request.Filepath, err)
|
||||
return nil, c.GetPermissionDeniedError()
|
||||
}
|
||||
|
@ -401,7 +401,7 @@ func (c *Connection) handleSFTPUploadToNewFile(fs vfs.Fs, pflags sftp.FileOpenFl
|
|||
return nil, c.GetQuotaExceededError()
|
||||
}
|
||||
|
||||
if err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, 0, 0); err != nil {
|
||||
if _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, 0, 0); err != nil {
|
||||
c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
||||
return nil, c.GetPermissionDeniedError()
|
||||
}
|
||||
|
@ -449,7 +449,7 @@ func (c *Connection) handleSFTPUploadToExistingFile(fs vfs.Fs, pflags sftp.FileO
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, fileSize, osFlags); err != nil {
|
||||
if _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, fileSize, osFlags); err != nil {
|
||||
c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
||||
return nil, c.GetPermissionDeniedError()
|
||||
}
|
||||
|
|
|
@ -236,7 +236,7 @@ func (c *scpCommand) handleUploadFile(fs vfs.Fs, resolvedPath, filePath string,
|
|||
c.sendErrorMessage(nil, err)
|
||||
return err
|
||||
}
|
||||
err := common.ExecutePreAction(c.connection.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath,
|
||||
_, err := common.ExecutePreAction(c.connection.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath,
|
||||
fileSize, os.O_TRUNC)
|
||||
if err != nil {
|
||||
c.connection.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
||||
|
@ -532,7 +532,7 @@ func (c *scpCommand) handleDownload(filePath string) error {
|
|||
return common.ErrPermissionDenied
|
||||
}
|
||||
|
||||
if err := common.ExecutePreAction(c.connection.BaseConnection, common.OperationPreDownload, p, filePath, 0, 0); err != nil {
|
||||
if _, err := common.ExecutePreAction(c.connection.BaseConnection, common.OperationPreDownload, p, filePath, 0, 0); err != nil {
|
||||
c.connection.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", filePath, err)
|
||||
c.sendErrorMessage(fs, common.ErrPermissionDenied)
|
||||
return common.ErrPermissionDenied
|
||||
|
|
|
@ -169,7 +169,7 @@ func (f *webDavFile) checkFirstRead() error {
|
|||
f.Connection.Log(logger.LevelWarn, "reading file %#v is not allowed", f.GetVirtualPath())
|
||||
return f.Connection.GetErrorForDeniedFile(policy)
|
||||
}
|
||||
err := common.ExecutePreAction(f.Connection, common.OperationPreDownload, f.GetFsPath(), f.GetVirtualPath(), 0, 0)
|
||||
_, err := common.ExecutePreAction(f.Connection, common.OperationPreDownload, f.GetFsPath(), f.GetVirtualPath(), 0, 0)
|
||||
if err != nil {
|
||||
f.Connection.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", f.GetVirtualPath(), err)
|
||||
return f.Connection.GetPermissionDeniedError()
|
||||
|
|
|
@ -211,7 +211,7 @@ func (c *Connection) handleUploadToNewFile(fs vfs.Fs, resolvedPath, filePath, re
|
|||
c.Log(logger.LevelInfo, "denying file write due to quota limits")
|
||||
return nil, common.ErrQuotaExceeded
|
||||
}
|
||||
if err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, 0, 0); err != nil {
|
||||
if _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, 0, 0); err != nil {
|
||||
c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
||||
return nil, c.GetPermissionDeniedError()
|
||||
}
|
||||
|
@ -243,7 +243,7 @@ func (c *Connection) handleUploadToExistingFile(fs vfs.Fs, resolvedPath, filePat
|
|||
c.Log(logger.LevelInfo, "denying file write due to quota limits")
|
||||
return nil, common.ErrQuotaExceeded
|
||||
}
|
||||
if err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath,
|
||||
if _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath,
|
||||
fileSize, os.O_TRUNC); err != nil {
|
||||
c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
|
||||
return nil, c.GetPermissionDeniedError()
|
||||
|
|
|
@ -350,7 +350,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<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>
|
||||
<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". Double asterisk is supported, for example "/**/*.txt" will match any file ending with ".txt". "/mydir/**" will match any entry in "/mydir"</h6>
|
||||
<div class="form-group row">
|
||||
<div class="col-md-12 form_field_fs_paths_outer">
|
||||
{{range $idx, $val := .Rule.Conditions.Options.FsPaths}}
|
||||
|
|
Loading…
Reference in a new issue