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 . Name filters
-
Shell-like pattern filters for usernames, folder names. For example "user*"" will match names starting with "user"
+
Shell-like pattern filters for usernames, folder names. For example "user*"" will match names starting with "user". For provider events, this filter is applied to the username of the admin executing the event.
{{range $idx, $val := .Rule.Conditions.Options.Names}} @@ -237,6 +237,60 @@ along with this program. If not, see .
+
+
+ Group name filters +
+
+
Shell-like pattern filters for group names. For example "group*"" will match group names starting with "group".
+
+
+ {{range $idx, $val := .Rule.Conditions.Options.GroupNames}} +
+
+ +
+
+ +
+
+ +
+
+ {{else}} +
+
+ +
+
+ +
+
+ +
+
+ {{end}} +
+
+ +
+ +
+
+
+
Path filters @@ -458,6 +512,36 @@ along with this program. If not, see . $(this).closest(".form_field_names_outer_row").remove(); }); + $("body").on("click", ".add_new_group_name_pattern_field_btn", function () { + var index = $(".form_field_group_names_outer").find(".form_field_group_names_outer_row").length; + while (document.getElementById("idGroupNamePattern"+index) != null){ + index++; + } + $(".form_field_group_names_outer").append(` +
+
+ +
+
+ +
+
+ +
+
+ `); + $("#idGroupNamePatternType"+index).selectpicker(); + }); + + $("body").on("click", ".remove_group_name_pattern_btn_frm_field", function () { + $(this).closest(".form_field_group_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){