diff --git a/docs/eventmanager.md b/docs/eventmanager.md
index 8ab6458e..5cf5b4f0 100644
--- a/docs/eventmanager.md
+++ b/docs/eventmanager.md
@@ -20,7 +20,7 @@ The following actions are supported:
The following placeholders are supported:
-- `{{Name}}`. Username, folder name or admin username for provider actions.
+- `{{Name}}`. Username, folder name or admin username for provider events.
- `{{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.
- `{{StatusString}}`. Status as string. Possible values "OK", "KO".
@@ -64,4 +64,5 @@ Some actions are not supported for some triggers, rules containing incompatible
- `Provider events`, user quota reset, transfer quota reset, data retention check and filesystem actions can be executed only if a user is updated. They will be executed for the affected user. Folder quota reset can be executed only for folders. Filesystem actions are not executed for `delete` user events because the actions is executed after the user deletion.
- `IP Blocked`, user quota reset, folder quota reset, transfer quota reset, data retention check and filesystem actions cannot be executed, we only have an IP.
- `Certificate`, user quota reset, folder quota reset, transfer quota reset, data retention check and filesystem actions cannot be executed.
-- `Email with attachments` are supported for filesystem events and provider events if a user is updated. We need a user to get the files to attach.
+- `Email with attachments` are supported for filesystem events and provider events if a user is added/updated. We need a user to get the files to attach.
+- `HTTP multipart requests with files as attachments` are supported for filesystem events and provider events if a user is added/updated. We need a user to get the files to attach.
diff --git a/internal/common/actions.go b/internal/common/actions.go
index 449cbd48..a7d86a56 100644
--- a/internal/common/actions.go
+++ b/internal/common/actions.go
@@ -124,6 +124,7 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua
if hasRules {
params := EventParams{
Name: notification.Username,
+ Groups: conn.User.Groups,
Event: notification.Action,
Status: notification.Status,
VirtualPath: notification.VirtualPath,
diff --git a/internal/common/eventmanager.go b/internal/common/eventmanager.go
index 23fafed2..c21ffef6 100644
--- a/internal/common/eventmanager.go
+++ b/internal/common/eventmanager.go
@@ -35,6 +35,7 @@ import (
"github.com/robfig/cron/v3"
"github.com/rs/xid"
+ "github.com/sftpgo/sdk"
mail "github.com/xhit/go-simple-mail/v2"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
@@ -267,6 +268,9 @@ func (r *eventRulesContainer) checkFsEventMatch(conditions dataprovider.EventCon
if !checkEventConditionPatterns(params.Name, conditions.Options.Names) {
return false
}
+ if !checkEventGroupConditionPatters(params.Groups, conditions.Options.GroupNames) {
+ return false
+ }
if !checkEventConditionPatterns(params.VirtualPath, conditions.Options.FsPaths) {
if !checkEventConditionPatterns(params.ObjectName, conditions.Options.FsPaths) {
return false
@@ -411,6 +415,7 @@ func (r *eventRulesContainer) handleCertificateEvent(params EventParams) {
// EventParams defines the supported event parameters
type EventParams struct {
Name string
+ Groups []sdk.GroupMapping
Event string
Status int
VirtualPath string
@@ -643,6 +648,21 @@ func checkEventConditionPatterns(name string, patterns []dataprovider.ConditionP
return false
}
+func checkEventGroupConditionPatters(groups []sdk.GroupMapping, patterns []dataprovider.ConditionPattern) bool {
+ if len(patterns) == 0 {
+ return true
+ }
+ for _, group := range groups {
+ for _, p := range patterns {
+ if checkEventConditionPattern(p, group.Name) {
+ return true
+ }
+ }
+ }
+
+ return false
+}
+
func getHTTPRuleActionEndpoint(c dataprovider.EventActionHTTPConfig, replacer *strings.Replacer) (string, error) {
if len(c.QueryParameters) > 0 {
u, err := url.Parse(c.Endpoint)
@@ -955,10 +975,17 @@ func executeDeleteFsRuleAction(deletes []string, replacer *strings.Replacer,
executed := 0
for _, user := range users {
// if sender is set, the conditions have already been evaluated
- if params.sender == "" && !checkEventConditionPatterns(user.Username, conditions.Names) {
- eventManagerLog(logger.LevelDebug, "skipping fs delete for user %s, name conditions don't match",
- user.Username)
- continue
+ if params.sender == "" {
+ if !checkEventConditionPatterns(user.Username, conditions.Names) {
+ eventManagerLog(logger.LevelDebug, "skipping fs delete for user %s, name conditions don't match",
+ user.Username)
+ continue
+ }
+ if !checkEventGroupConditionPatters(user.Groups, conditions.GroupNames) {
+ eventManagerLog(logger.LevelDebug, "skipping fs delete for user %s, group name conditions don't match",
+ user.Username)
+ continue
+ }
}
executed++
if err = executeDeleteFsActionForUser(deletes, replacer, user); err != nil {
@@ -1013,10 +1040,17 @@ func executeMkdirFsRuleAction(dirs []string, replacer *strings.Replacer,
executed := 0
for _, user := range users {
// if sender is set, the conditions have already been evaluated
- if params.sender == "" && !checkEventConditionPatterns(user.Username, conditions.Names) {
- eventManagerLog(logger.LevelDebug, "skipping fs mkdir for user %s, name conditions don't match",
- user.Username)
- continue
+ if params.sender == "" {
+ if !checkEventConditionPatterns(user.Username, conditions.Names) {
+ eventManagerLog(logger.LevelDebug, "skipping fs mkdir for user %s, name conditions don't match",
+ user.Username)
+ continue
+ }
+ if !checkEventGroupConditionPatters(user.Groups, conditions.GroupNames) {
+ eventManagerLog(logger.LevelDebug, "skipping fs mkdir for user %s, group name conditions don't match",
+ user.Username)
+ continue
+ }
}
executed++
if err = executeMkDirsFsActionForUser(dirs, replacer, user); err != nil {
@@ -1094,10 +1128,17 @@ func executeRenameFsRuleAction(renames []dataprovider.KeyValue, replacer *string
executed := 0
for _, user := range users {
// if sender is set, the conditions have already been evaluated
- if params.sender == "" && !checkEventConditionPatterns(user.Username, conditions.Names) {
- eventManagerLog(logger.LevelDebug, "skipping fs rename for user %s, name conditions don't match",
- user.Username)
- continue
+ if params.sender == "" {
+ if !checkEventConditionPatterns(user.Username, conditions.Names) {
+ eventManagerLog(logger.LevelDebug, "skipping fs rename for user %s, name conditions don't match",
+ user.Username)
+ continue
+ }
+ if !checkEventGroupConditionPatters(user.Groups, conditions.GroupNames) {
+ eventManagerLog(logger.LevelDebug, "skipping fs rename for user %s, group name conditions don't match",
+ user.Username)
+ continue
+ }
}
executed++
if err = executeRenameFsActionForUser(renames, replacer, user); err != nil {
@@ -1127,10 +1168,17 @@ func executeExistFsRuleAction(exist []string, replacer *strings.Replacer, condit
executed := 0
for _, user := range users {
// if sender is set, the conditions have already been evaluated
- if params.sender == "" && !checkEventConditionPatterns(user.Username, conditions.Names) {
- eventManagerLog(logger.LevelDebug, "skipping fs exist for user %s, name conditions don't match",
- user.Username)
- continue
+ if params.sender == "" {
+ if !checkEventConditionPatterns(user.Username, conditions.Names) {
+ eventManagerLog(logger.LevelDebug, "skipping fs exist for user %s, name conditions don't match",
+ user.Username)
+ continue
+ }
+ if !checkEventGroupConditionPatters(user.Groups, conditions.GroupNames) {
+ eventManagerLog(logger.LevelDebug, "skipping fs exist for user %s, group name conditions don't match",
+ user.Username)
+ continue
+ }
}
executed++
if err = executeExistFsActionForUser(exist, replacer, user); err != nil {
@@ -1203,10 +1251,17 @@ func executeUsersQuotaResetRuleAction(conditions dataprovider.ConditionOptions,
executed := 0
for _, user := range users {
// if sender is set, the conditions have already been evaluated
- if params.sender == "" && !checkEventConditionPatterns(user.Username, conditions.Names) {
- eventManagerLog(logger.LevelDebug, "skipping quota reset for user %q, name conditions don't match",
- user.Username)
- continue
+ if params.sender == "" {
+ if !checkEventConditionPatterns(user.Username, conditions.Names) {
+ eventManagerLog(logger.LevelDebug, "skipping quota reset for user %q, name conditions don't match",
+ user.Username)
+ continue
+ }
+ if !checkEventGroupConditionPatters(user.Groups, conditions.GroupNames) {
+ eventManagerLog(logger.LevelDebug, "skipping quota reset for user %q, group name conditions don't match",
+ user.Username)
+ continue
+ }
}
executed++
if err = executeQuotaResetForUser(user); err != nil {
@@ -1284,10 +1339,17 @@ func executeTransferQuotaResetRuleAction(conditions dataprovider.ConditionOption
executed := 0
for _, user := range users {
// if sender is set, the conditions have already been evaluated
- if params.sender == "" && !checkEventConditionPatterns(user.Username, conditions.Names) {
- eventManagerLog(logger.LevelDebug, "skipping scheduled transfer quota reset for user %s, name conditions don't match",
- user.Username)
- continue
+ if params.sender == "" {
+ if !checkEventConditionPatterns(user.Username, conditions.Names) {
+ eventManagerLog(logger.LevelDebug, "skipping scheduled transfer quota reset for user %s, name conditions don't match",
+ user.Username)
+ continue
+ }
+ if !checkEventGroupConditionPatters(user.Groups, conditions.GroupNames) {
+ eventManagerLog(logger.LevelDebug, "skipping scheduled transfer quota reset for user %s, group name conditions don't match",
+ user.Username)
+ continue
+ }
}
executed++
err = dataprovider.UpdateUserTransferQuota(&user, 0, 0, true)
@@ -1339,10 +1401,17 @@ func executeDataRetentionCheckRuleAction(config dataprovider.EventActionDataRete
executed := 0
for _, user := range users {
// if sender is set, the conditions have already been evaluated
- if params.sender == "" && !checkEventConditionPatterns(user.Username, conditions.Names) {
- eventManagerLog(logger.LevelDebug, "skipping scheduled retention check for user %s, name conditions don't match",
- user.Username)
- continue
+ if params.sender == "" {
+ if !checkEventConditionPatterns(user.Username, conditions.Names) {
+ eventManagerLog(logger.LevelDebug, "skipping scheduled retention check for user %s, name conditions don't match",
+ user.Username)
+ continue
+ }
+ if !checkEventGroupConditionPatters(user.Groups, conditions.GroupNames) {
+ eventManagerLog(logger.LevelDebug, "skipping scheduled retention check for user %s, group name conditions don't match",
+ user.Username)
+ continue
+ }
}
executed++
if err = executeDataRetentionCheckForUser(user, config.Folders); err != nil {
diff --git a/internal/common/eventmanager_test.go b/internal/common/eventmanager_test.go
index 7fb839ba..304cef30 100644
--- a/internal/common/eventmanager_test.go
+++ b/internal/common/eventmanager_test.go
@@ -148,6 +148,50 @@ func TestEventRuleMatch(t *testing.T) {
}
res = eventManager.checkFsEventMatch(conditions, params)
assert.False(t, res)
+ // check fs events with group name filters
+ conditions = dataprovider.EventConditions{
+ FsEvents: []string{operationUpload, operationDownload},
+ Options: dataprovider.ConditionOptions{
+ GroupNames: []dataprovider.ConditionPattern{
+ {
+ Pattern: "group*",
+ },
+ {
+ Pattern: "testgroup*",
+ },
+ },
+ },
+ }
+ params = EventParams{
+ Name: "user1",
+ Event: operationUpload,
+ }
+ res = eventManager.checkFsEventMatch(conditions, params)
+ assert.False(t, res)
+ params.Groups = []sdk.GroupMapping{
+ {
+ Name: "g1",
+ Type: sdk.GroupTypePrimary,
+ },
+ {
+ Name: "g2",
+ Type: sdk.GroupTypeSecondary,
+ },
+ }
+ res = eventManager.checkFsEventMatch(conditions, params)
+ assert.False(t, res)
+ params.Groups = []sdk.GroupMapping{
+ {
+ Name: "testgroup2",
+ Type: sdk.GroupTypePrimary,
+ },
+ {
+ Name: "g2",
+ Type: sdk.GroupTypeSecondary,
+ },
+ }
+ res = eventManager.checkFsEventMatch(conditions, params)
+ assert.True(t, res)
}
func TestEventManager(t *testing.T) {
@@ -902,6 +946,62 @@ func TestEventRuleActions(t *testing.T) {
assert.NoError(t, err)
}
+func TestEventRuleActionsNoGroupMatching(t *testing.T) {
+ username := "test_user_action_group_matching"
+ user := dataprovider.User{
+ BaseUser: sdk.BaseUser{
+ Username: username,
+ Permissions: map[string][]string{
+ "/": {dataprovider.PermAny},
+ },
+ HomeDir: filepath.Join(os.TempDir(), username),
+ },
+ }
+ err := dataprovider.AddUser(&user, "", "")
+ assert.NoError(t, err)
+
+ conditions := dataprovider.ConditionOptions{
+ GroupNames: []dataprovider.ConditionPattern{
+ {
+ Pattern: "agroup",
+ },
+ },
+ }
+ err = executeDeleteFsRuleAction(nil, nil, conditions, &EventParams{})
+ if assert.Error(t, err) {
+ assert.Contains(t, err.Error(), "no delete executed")
+ }
+ err = executeMkdirFsRuleAction(nil, nil, conditions, &EventParams{})
+ if assert.Error(t, err) {
+ assert.Contains(t, err.Error(), "no mkdir executed")
+ }
+ err = executeRenameFsRuleAction(nil, nil, conditions, &EventParams{})
+ if assert.Error(t, err) {
+ assert.Contains(t, err.Error(), "no rename executed")
+ }
+ err = executeExistFsRuleAction(nil, nil, conditions, &EventParams{})
+ if assert.Error(t, err) {
+ assert.Contains(t, err.Error(), "no existence check executed")
+ }
+ err = executeUsersQuotaResetRuleAction(conditions, &EventParams{})
+ if assert.Error(t, err) {
+ assert.Contains(t, err.Error(), "no user quota reset executed")
+ }
+ err = executeTransferQuotaResetRuleAction(conditions, &EventParams{})
+ if assert.Error(t, err) {
+ assert.Contains(t, err.Error(), "no transfer quota reset executed")
+ }
+ err = executeDataRetentionCheckRuleAction(dataprovider.EventActionDataRetentionConfig{}, conditions, &EventParams{})
+ if assert.Error(t, err) {
+ assert.Contains(t, err.Error(), "no retention check executed")
+ }
+
+ err = dataprovider.DeleteUser(username, "", "")
+ assert.NoError(t, err)
+ err = os.RemoveAll(user.GetHomeDir())
+ assert.NoError(t, err)
+}
+
func TestGetFileContent(t *testing.T) {
username := "test_user_get_file_content"
user := dataprovider.User{
diff --git a/internal/common/protocol_test.go b/internal/common/protocol_test.go
index 980ac9b8..6bff5980 100644
--- a/internal/common/protocol_test.go
+++ b/internal/common/protocol_test.go
@@ -3825,6 +3825,136 @@ func TestEventRuleFsActions(t *testing.T) {
assert.NoError(t, err)
}
+func TestEventFsActionsGroupFilters(t *testing.T) {
+ smtpCfg := smtp.Config{
+ Host: "127.0.0.1",
+ Port: 2525,
+ From: "notification@example.com",
+ TemplatesPath: "templates",
+ }
+ err := smtpCfg.Initialize(configDir)
+ require.NoError(t, err)
+
+ a1 := dataprovider.BaseEventAction{
+ Name: "a1",
+ Type: dataprovider.ActionTypeEmail,
+ Options: dataprovider.BaseEventActionOptions{
+ EmailConfig: dataprovider.EventActionEmailConfig{
+ Recipients: []string{"example@example.net"},
+ Subject: `New "{{Event}}" from "{{Name}}" status {{StatusString}}`,
+ Body: "Fs path {{FsPath}}, size: {{FileSize}}, protocol: {{Protocol}}, IP: {{IP}} {{ErrorString}}",
+ },
+ },
+ }
+ action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)
+ assert.NoError(t, err)
+
+ r1 := dataprovider.EventRule{
+ Name: "rule1",
+ Trigger: dataprovider.EventTriggerFsEvent,
+ Conditions: dataprovider.EventConditions{
+ FsEvents: []string{"upload"},
+ Options: dataprovider.ConditionOptions{
+ GroupNames: []dataprovider.ConditionPattern{
+ {
+ Pattern: "group*",
+ },
+ },
+ },
+ },
+ Actions: []dataprovider.EventAction{
+ {
+ BaseEventAction: dataprovider.BaseEventAction{
+ Name: action1.Name,
+ },
+ Order: 1,
+ Options: dataprovider.EventActionOptions{
+ ExecuteSync: true,
+ },
+ },
+ },
+ }
+ rule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)
+ assert.NoError(t, err)
+ user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
+ assert.NoError(t, err)
+ conn, client, err := getSftpClient(user)
+ if assert.NoError(t, err) {
+ defer conn.Close()
+ defer client.Close()
+ // the user has no group, so the rule does not match
+ lastReceivedEmail.reset()
+ err = writeSFTPFile(testFileName, 32, client)
+ assert.NoError(t, err)
+ assert.Empty(t, lastReceivedEmail.get().From)
+ }
+ g1 := dataprovider.Group{
+ BaseGroup: sdk.BaseGroup{
+ Name: "agroup1",
+ },
+ }
+ group1, _, err := httpdtest.AddGroup(g1, http.StatusCreated)
+ assert.NoError(t, err)
+
+ g2 := dataprovider.Group{
+ BaseGroup: sdk.BaseGroup{
+ Name: "group2",
+ },
+ }
+ group2, _, err := httpdtest.AddGroup(g2, http.StatusCreated)
+ assert.NoError(t, err)
+ user.Groups = []sdk.GroupMapping{
+ {
+ Name: group1.Name,
+ Type: sdk.GroupTypePrimary,
+ },
+ }
+ _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
+ assert.NoError(t, err)
+ conn, client, err = getSftpClient(user)
+ if assert.NoError(t, err) {
+ defer conn.Close()
+ defer client.Close()
+ // the group does not match
+ lastReceivedEmail.reset()
+ err = writeSFTPFile(testFileName, 32, client)
+ assert.NoError(t, err)
+ assert.Empty(t, lastReceivedEmail.get().From)
+ }
+ user.Groups = append(user.Groups, sdk.GroupMapping{
+ Name: group2.Name,
+ Type: sdk.GroupTypeSecondary,
+ })
+ _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
+ assert.NoError(t, err)
+ conn, client, err = getSftpClient(user)
+ if assert.NoError(t, err) {
+ defer conn.Close()
+ defer client.Close()
+ // the group matches
+ lastReceivedEmail.reset()
+ err = writeSFTPFile(testFileName, 32, client)
+ assert.NoError(t, err)
+ assert.NotEmpty(t, lastReceivedEmail.get().From)
+ }
+ _, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)
+ assert.NoError(t, err)
+ _, err = httpdtest.RemoveEventAction(action1, http.StatusOK)
+ assert.NoError(t, err)
+ _, err = httpdtest.RemoveUser(user, http.StatusOK)
+ assert.NoError(t, err)
+ err = os.RemoveAll(user.GetHomeDir())
+ assert.NoError(t, err)
+ _, err = httpdtest.RemoveGroup(group1, http.StatusOK)
+ assert.NoError(t, err)
+ _, err = httpdtest.RemoveGroup(group2, http.StatusOK)
+ assert.NoError(t, err)
+
+ smtpCfg = smtp.Config{}
+ err = smtpCfg.Initialize(configDir)
+ require.NoError(t, err)
+}
+
func TestEventActionHTTPMultipart(t *testing.T) {
a1 := dataprovider.BaseEventAction{
Name: "action1",
diff --git a/internal/dataprovider/eventrule.go b/internal/dataprovider/eventrule.go
index ceca4cdb..bb940606 100644
--- a/internal/dataprovider/eventrule.go
+++ b/internal/dataprovider/eventrule.go
@@ -930,6 +930,8 @@ func (p *ConditionPattern) validate() error {
type ConditionOptions struct {
// Usernames or folder names
Names []ConditionPattern `json:"names,omitempty"`
+ // Group names
+ GroupNames []ConditionPattern `json:"group_names,omitempty"`
// Virtual paths
FsPaths []ConditionPattern `json:"fs_paths,omitempty"`
Protocols []string `json:"protocols,omitempty"`
@@ -948,6 +950,7 @@ func (f *ConditionOptions) getACopy() ConditionOptions {
return ConditionOptions{
Names: cloneConditionPatterns(f.Names),
+ GroupNames: cloneConditionPatterns(f.GroupNames),
FsPaths: cloneConditionPatterns(f.FsPaths),
Protocols: protocols,
ProviderObjects: providerObjects,
@@ -963,6 +966,11 @@ func (f *ConditionOptions) validate() error {
return err
}
}
+ for _, name := range f.GroupNames {
+ if err := name.validate(); err != nil {
+ return err
+ }
+ }
for _, fsPath := range f.FsPaths {
if err := fsPath.validate(); err != nil {
return err
@@ -1061,6 +1069,7 @@ func (c *EventConditions) validate(trigger int) error {
case EventTriggerProviderEvent:
c.FsEvents = nil
c.Schedules = nil
+ c.Options.GroupNames = nil
c.Options.FsPaths = nil
c.Options.Protocols = nil
c.Options.MinFileSize = 0
@@ -1093,6 +1102,7 @@ func (c *EventConditions) validate(trigger int) error {
c.FsEvents = nil
c.ProviderEvents = nil
c.Options.Names = nil
+ c.Options.GroupNames = nil
c.Options.FsPaths = nil
c.Options.Protocols = nil
c.Options.MinFileSize = 0
@@ -1101,6 +1111,7 @@ func (c *EventConditions) validate(trigger int) error {
default:
c.FsEvents = nil
c.ProviderEvents = nil
+ c.Options.GroupNames = nil
c.Options.FsPaths = nil
c.Options.Protocols = nil
c.Options.MinFileSize = 0
diff --git a/internal/httpd/httpd_test.go b/internal/httpd/httpd_test.go
index 11e42184..f53f56c6 100644
--- a/internal/httpd/httpd_test.go
+++ b/internal/httpd/httpd_test.go
@@ -19250,6 +19250,12 @@ func TestWebEventRule(t *testing.T) {
InverseMatch: true,
},
},
+ GroupNames: []dataprovider.ConditionPattern{
+ {
+ Pattern: "g*",
+ InverseMatch: true,
+ },
+ },
},
},
Actions: []dataprovider.EventAction{
@@ -19279,6 +19285,8 @@ func TestWebEventRule(t *testing.T) {
form.Set("schedule_month0", rule.Conditions.Schedules[0].Month)
form.Set("name_pattern0", rule.Conditions.Options.Names[0].Pattern)
form.Set("type_name_pattern0", "inverse")
+ form.Set("group_name_pattern0", rule.Conditions.Options.GroupNames[0].Pattern)
+ form.Set("type_group_name_pattern0", "inverse")
req, err = http.NewRequest(http.MethodPost, webAdminEventRulePath, bytes.NewBuffer([]byte(form.Encode())))
assert.NoError(t, err)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
@@ -19371,6 +19379,12 @@ func TestWebEventRule(t *testing.T) {
InverseMatch: true,
},
},
+ GroupNames: []dataprovider.ConditionPattern{
+ {
+ Pattern: "g*",
+ InverseMatch: true,
+ },
+ },
FsPaths: []dataprovider.ConditionPattern{
{
Pattern: "/subdir/*.txt",
diff --git a/internal/httpd/webadmin.go b/internal/httpd/webadmin.go
index 461fa85d..cfed7f39 100644
--- a/internal/httpd/webadmin.go
+++ b/internal/httpd/webadmin.go
@@ -109,6 +109,7 @@ const (
pageResetPwdTitle = "SFTPGo Admin - Reset password"
pageSetupTitle = "Create first admin user"
defaultQueryLimit = 500
+ inversePatternType = "inverse"
)
var (
@@ -2006,7 +2007,7 @@ func getEventActionFromPostFields(r *http.Request) (dataprovider.BaseEventAction
func getEventRuleConditionsFromPostFields(r *http.Request) (dataprovider.EventConditions, error) {
var schedules []dataprovider.Schedule
- var names, fsPaths []dataprovider.ConditionPattern
+ var names, groupNames, fsPaths []dataprovider.ConditionPattern
for k := range r.Form {
if strings.HasPrefix(k, "schedule_hour") {
hour := r.Form.Get(k)
@@ -2030,7 +2031,18 @@ func getEventRuleConditionsFromPostFields(r *http.Request) (dataprovider.EventCo
patternType := r.Form.Get(fmt.Sprintf("type_name_pattern%s", idx))
names = append(names, dataprovider.ConditionPattern{
Pattern: pattern,
- InverseMatch: patternType == "inverse",
+ InverseMatch: patternType == inversePatternType,
+ })
+ }
+ }
+ if strings.HasPrefix(k, "group_name_pattern") {
+ pattern := r.Form.Get(k)
+ if pattern != "" {
+ idx := strings.TrimPrefix(k, "group_name_pattern")
+ patternType := r.Form.Get(fmt.Sprintf("type_group_name_pattern%s", idx))
+ groupNames = append(groupNames, dataprovider.ConditionPattern{
+ Pattern: pattern,
+ InverseMatch: patternType == inversePatternType,
})
}
}
@@ -2041,7 +2053,7 @@ func getEventRuleConditionsFromPostFields(r *http.Request) (dataprovider.EventCo
patternType := r.Form.Get(fmt.Sprintf("type_fs_path_pattern%s", idx))
fsPaths = append(fsPaths, dataprovider.ConditionPattern{
Pattern: pattern,
- InverseMatch: patternType == "inverse",
+ InverseMatch: patternType == inversePatternType,
})
}
}
@@ -2060,6 +2072,7 @@ func getEventRuleConditionsFromPostFields(r *http.Request) (dataprovider.EventCo
Schedules: schedules,
Options: dataprovider.ConditionOptions{
Names: names,
+ GroupNames: groupNames,
FsPaths: fsPaths,
Protocols: r.Form["fs_protocols"],
ProviderObjects: r.Form["provider_objects"],
diff --git a/internal/httpdtest/httpdtest.go b/internal/httpdtest/httpdtest.go
index 578afc5e..3e1b2dea 100644
--- a/internal/httpdtest/httpdtest.go
+++ b/internal/httpdtest/httpdtest.go
@@ -1397,6 +1397,9 @@ func checkEventConditionOptions(expected, actual dataprovider.ConditionOptions)
if err := compareConditionPatternOptions(expected.Names, actual.Names); err != nil {
return errors.New("condition names mismatch")
}
+ if err := compareConditionPatternOptions(expected.GroupNames, actual.GroupNames); err != nil {
+ return errors.New("condition group names mismatch")
+ }
if err := compareConditionPatternOptions(expected.FsPaths, actual.FsPaths); err != nil {
return errors.New("condition fs_paths mismatch")
}
diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml
index 3d32828c..46410176 100644
--- a/openapi/openapi.yaml
+++ b/openapi/openapi.yaml
@@ -6211,6 +6211,10 @@ components:
type: array
items:
$ref: '#/components/schemas/ConditionPattern'
+ group_names:
+ type: array
+ items:
+ $ref: '#/components/schemas/ConditionPattern'
fs_paths:
type: array
items:
diff --git a/templates/webadmin/eventrule.html b/templates/webadmin/eventrule.html
index 9346cba3..d2c8ecd3 100644
--- a/templates/webadmin/eventrule.html
+++ b/templates/webadmin/eventrule.html
@@ -188,7 +188,7 @@ along with this program. If not, see