mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-25 00:50:31 +00:00
event rules: allow filtering based on group names
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
bf76b0b158
commit
f264b005ff
11 changed files with 464 additions and 34 deletions
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -188,7 +188,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<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>
|
||||
<h6 class="card-title mb-4">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.</h6>
|
||||
<div class="form-group row">
|
||||
<div class="col-md-12 form_field_names_outer">
|
||||
{{range $idx, $val := .Rule.Conditions.Options.Names}}
|
||||
|
@ -237,6 +237,60 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-light mb-3 trigger trigger-fs trigger-schedule">
|
||||
<div class="card-header">
|
||||
<b>Group name filters</b>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6 class="card-title mb-4">Shell-like pattern filters for group names. For example "group*"" will match group names starting with "group".</h6>
|
||||
<div class="form-group row">
|
||||
<div class="col-md-12 form_field_group_names_outer">
|
||||
{{range $idx, $val := .Rule.Conditions.Options.GroupNames}}
|
||||
<div class="row form_field_group_names_outer_row">
|
||||
<div class="form-group col-md-8">
|
||||
<input type="text" class="form-control" id="idGroupNamePattern{{$idx}}" name="group_name_pattern{{$idx}}" placeholder="" value="{{$val.Pattern}}" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<select class="form-control selectpicker" id="idGroupNamePatternType{{$idx}}" name="type_group_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_group_name_pattern_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="row form_field_group_names_outer_row">
|
||||
<div class="form-group col-md-8">
|
||||
<input type="text" class="form-control" id="idGroupNamePattern0" name="group_name_pattern0" placeholder="" value="" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<select class="form-control selectpicker" id="idGroupNamePatternType0" name="type_group_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_group_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_group_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>
|
||||
|
@ -458,6 +512,36 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
$(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(`
|
||||
<div class="row form_field_group_names_outer_row">
|
||||
<div class="form-group col-md-8">
|
||||
<input type="text" class="form-control" id="idGroupNamePattern${index}" name="group_name_pattern${index}" placeholder="" value="" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<select class="form-control" id="idGroupNamePatternType${index}" name="type_group_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_group_name_pattern_btn_frm_field">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
$("#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){
|
||||
|
|
Loading…
Reference in a new issue