Browse Source

eventmanager: allow to filter based on role name

Signed-off-by: Nicola Murino <[email protected]>
Nicola Murino 2 years ago
parent
commit
221a4878aa

+ 37 - 63
internal/common/eventmanager.go

@@ -262,6 +262,9 @@ func (r *eventRulesContainer) checkProviderEventMatch(conditions dataprovider.Ev
 	if !checkEventConditionPatterns(params.Name, conditions.Options.Names) {
 		return false
 	}
+	if !checkEventConditionPatterns(params.Role, conditions.Options.RoleNames) {
+		return false
+	}
 	if len(conditions.Options.ProviderObjects) > 0 && !util.Contains(conditions.Options.ProviderObjects, params.ObjectType) {
 		return false
 	}
@@ -275,6 +278,9 @@ func (r *eventRulesContainer) checkFsEventMatch(conditions dataprovider.EventCon
 	if !checkEventConditionPatterns(params.Name, conditions.Options.Names) {
 		return false
 	}
+	if !checkEventConditionPatterns(params.Role, conditions.Options.RoleNames) {
+		return false
+	}
 	if !checkEventGroupConditionPatters(params.Groups, conditions.Options.GroupNames) {
 		return false
 	}
@@ -919,6 +925,19 @@ func checkEventConditionPattern(p dataprovider.ConditionPattern, name string) bo
 	return matched
 }
 
+func checkUserConditionOptions(user *dataprovider.User, conditions *dataprovider.ConditionOptions) bool {
+	if !checkEventConditionPatterns(user.Username, conditions.Names) {
+		return false
+	}
+	if !checkEventConditionPatterns(user.Role, conditions.RoleNames) {
+		return false
+	}
+	if !checkEventGroupConditionPatters(user.Groups, conditions.GroupNames) {
+		return false
+	}
+	return true
+}
+
 // checkConditionPatterns returns false if patterns are defined and no match is found
 func checkEventConditionPatterns(name string, patterns []dataprovider.ConditionPattern) bool {
 	if len(patterns) == 0 {
@@ -1302,13 +1321,8 @@ func executeDeleteFsRuleAction(deletes []string, replacer *strings.Replacer,
 	for _, user := range users {
 		// if sender is set, the conditions have already been evaluated
 		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",
+			if !checkUserConditionOptions(&user, &conditions) {
+				eventManagerLog(logger.LevelDebug, "skipping fs delete for user %s, condition options don't match",
 					user.Username)
 				continue
 			}
@@ -1366,13 +1380,8 @@ func executeMkdirFsRuleAction(dirs []string, replacer *strings.Replacer,
 	for _, user := range users {
 		// if sender is set, the conditions have already been evaluated
 		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",
+			if !checkUserConditionOptions(&user, &conditions) {
+				eventManagerLog(logger.LevelDebug, "skipping fs mkdir for user %s, condition options don't match",
 					user.Username)
 				continue
 			}
@@ -1453,13 +1462,8 @@ func executeRenameFsRuleAction(renames []dataprovider.KeyValue, replacer *string
 	for _, user := range users {
 		// if sender is set, the conditions have already been evaluated
 		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",
+			if !checkUserConditionOptions(&user, &conditions) {
+				eventManagerLog(logger.LevelDebug, "skipping fs rename for user %s, condition options don't match",
 					user.Username)
 				continue
 			}
@@ -1559,13 +1563,8 @@ func executeExistFsRuleAction(exist []string, replacer *strings.Replacer, condit
 	for _, user := range users {
 		// if sender is set, the conditions have already been evaluated
 		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",
+			if !checkUserConditionOptions(&user, &conditions) {
+				eventManagerLog(logger.LevelDebug, "skipping fs exist for user %s, condition options don't match",
 					user.Username)
 				continue
 			}
@@ -1599,13 +1598,8 @@ func executeCompressFsRuleAction(c dataprovider.EventActionFsCompress, replacer
 	for _, user := range users {
 		// if sender is set, the conditions have already been evaluated
 		if params.sender == "" {
-			if !checkEventConditionPatterns(user.Username, conditions.Names) {
-				eventManagerLog(logger.LevelDebug, "skipping fs compress for user %s, name conditions don't match",
-					user.Username)
-				continue
-			}
-			if !checkEventGroupConditionPatters(user.Groups, conditions.GroupNames) {
-				eventManagerLog(logger.LevelDebug, "skipping fs compress for user %s, group name conditions don't match",
+			if !checkUserConditionOptions(&user, &conditions) {
+				eventManagerLog(logger.LevelDebug, "skipping fs compress for user %s, condition options don't match",
 					user.Username)
 				continue
 			}
@@ -1684,13 +1678,8 @@ func executeUsersQuotaResetRuleAction(conditions dataprovider.ConditionOptions,
 	for _, user := range users {
 		// if sender is set, the conditions have already been evaluated
 		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",
+			if !checkUserConditionOptions(&user, &conditions) {
+				eventManagerLog(logger.LevelDebug, "skipping quota reset for user %q, condition options don't match",
 					user.Username)
 				continue
 			}
@@ -1772,13 +1761,8 @@ func executeTransferQuotaResetRuleAction(conditions dataprovider.ConditionOption
 	for _, user := range users {
 		// if sender is set, the conditions have already been evaluated
 		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",
+			if !checkUserConditionOptions(&user, &conditions) {
+				eventManagerLog(logger.LevelDebug, "skipping scheduled transfer quota reset for user %s, condition options don't match",
 					user.Username)
 				continue
 			}
@@ -1843,13 +1827,8 @@ func executeDataRetentionCheckRuleAction(config dataprovider.EventActionDataRete
 	for _, user := range users {
 		// if sender is set, the conditions have already been evaluated
 		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",
+			if !checkUserConditionOptions(&user, &conditions) {
+				eventManagerLog(logger.LevelDebug, "skipping scheduled retention check for user %s, condition options don't match",
 					user.Username)
 				continue
 			}
@@ -1900,13 +1879,8 @@ func executeMetadataCheckRuleAction(conditions dataprovider.ConditionOptions, pa
 	for _, user := range users {
 		// if sender is set, the conditions have already been evaluated
 		if params.sender == "" {
-			if !checkEventConditionPatterns(user.Username, conditions.Names) {
-				eventManagerLog(logger.LevelDebug, "skipping metadata check for user %q, name conditions don't match",
-					user.Username)
-				continue
-			}
-			if !checkEventGroupConditionPatters(user.Groups, conditions.GroupNames) {
-				eventManagerLog(logger.LevelDebug, "skipping metadata check for user %q, group name conditions don't match",
+			if !checkUserConditionOptions(&user, &conditions) {
+				eventManagerLog(logger.LevelDebug, "skipping metadata check for user %q, condition options don't match",
 					user.Username)
 				continue
 			}

+ 71 - 0
internal/common/eventmanager_test.go

@@ -42,6 +42,7 @@ import (
 )
 
 func TestEventRuleMatch(t *testing.T) {
+	role := "role1"
 	conditions := dataprovider.EventConditions{
 		ProviderEvents: []string{"add", "update"},
 		Options: dataprovider.ConditionOptions{
@@ -51,20 +52,28 @@ func TestEventRuleMatch(t *testing.T) {
 					InverseMatch: true,
 				},
 			},
+			RoleNames: []dataprovider.ConditionPattern{
+				{
+					Pattern: role,
+				},
+			},
 		},
 	}
 	res := eventManager.checkProviderEventMatch(conditions, EventParams{
 		Name:  "user1",
+		Role:  role,
 		Event: "add",
 	})
 	assert.False(t, res)
 	res = eventManager.checkProviderEventMatch(conditions, EventParams{
 		Name:  "user2",
+		Role:  role,
 		Event: "update",
 	})
 	assert.True(t, res)
 	res = eventManager.checkProviderEventMatch(conditions, EventParams{
 		Name:  "user2",
+		Role:  role,
 		Event: "delete",
 	})
 	assert.False(t, res)
@@ -72,15 +81,24 @@ func TestEventRuleMatch(t *testing.T) {
 	res = eventManager.checkProviderEventMatch(conditions, EventParams{
 		Name:       "user2",
 		Event:      "update",
+		Role:       role,
 		ObjectType: "share",
 	})
 	assert.False(t, res)
 	res = eventManager.checkProviderEventMatch(conditions, EventParams{
 		Name:       "user2",
 		Event:      "update",
+		Role:       role,
 		ObjectType: "api_key",
 	})
 	assert.True(t, res)
+	res = eventManager.checkProviderEventMatch(conditions, EventParams{
+		Name:       "user2",
+		Event:      "update",
+		Role:       role + "1",
+		ObjectType: "api_key",
+	})
+	assert.False(t, res)
 	// now test fs events
 	conditions = dataprovider.EventConditions{
 		FsEvents: []string{operationUpload, operationDownload},
@@ -93,6 +111,12 @@ func TestEventRuleMatch(t *testing.T) {
 					Pattern: "tester*",
 				},
 			},
+			RoleNames: []dataprovider.ConditionPattern{
+				{
+					Pattern:      role,
+					InverseMatch: true,
+				},
+			},
 			FsPaths: []dataprovider.ConditionPattern{
 				{
 					Pattern: "*.txt",
@@ -116,6 +140,10 @@ func TestEventRuleMatch(t *testing.T) {
 	params.Event = operationDownload
 	res = eventManager.checkFsEventMatch(conditions, params)
 	assert.True(t, res)
+	params.Role = role
+	res = eventManager.checkFsEventMatch(conditions, params)
+	assert.False(t, res)
+	params.Role = ""
 	params.Name = "name"
 	res = eventManager.checkFsEventMatch(conditions, params)
 	assert.False(t, res)
@@ -195,6 +223,49 @@ func TestEventRuleMatch(t *testing.T) {
 	}
 	res = eventManager.checkFsEventMatch(conditions, params)
 	assert.True(t, res)
+	// check user conditions
+	user := dataprovider.User{}
+	user.Username = "u1"
+	res = checkUserConditionOptions(&user, &dataprovider.ConditionOptions{})
+	assert.True(t, res)
+	res = checkUserConditionOptions(&user, &dataprovider.ConditionOptions{
+		Names: []dataprovider.ConditionPattern{
+			{
+				Pattern: "user",
+			},
+		},
+	})
+	assert.False(t, res)
+	res = checkUserConditionOptions(&user, &dataprovider.ConditionOptions{
+		RoleNames: []dataprovider.ConditionPattern{
+			{
+				Pattern: role,
+			},
+		},
+	})
+	assert.False(t, res)
+	user.Role = role
+	res = checkUserConditionOptions(&user, &dataprovider.ConditionOptions{
+		RoleNames: []dataprovider.ConditionPattern{
+			{
+				Pattern: role,
+			},
+		},
+	})
+	assert.True(t, res)
+	res = checkUserConditionOptions(&user, &dataprovider.ConditionOptions{
+		GroupNames: []dataprovider.ConditionPattern{
+			{
+				Pattern: "group",
+			},
+		},
+		RoleNames: []dataprovider.ConditionPattern{
+			{
+				Pattern: role,
+			},
+		},
+	})
+	assert.False(t, res)
 }
 
 func TestEventManager(t *testing.T) {

+ 24 - 12
internal/dataprovider/eventrule.go

@@ -1021,6 +1021,8 @@ type ConditionOptions struct {
 	Names []ConditionPattern `json:"names,omitempty"`
 	// Group names
 	GroupNames []ConditionPattern `json:"group_names,omitempty"`
+	// Role names
+	RoleNames []ConditionPattern `json:"role_names,omitempty"`
 	// Virtual paths
 	FsPaths         []ConditionPattern `json:"fs_paths,omitempty"`
 	Protocols       []string           `json:"protocols,omitempty"`
@@ -1040,6 +1042,7 @@ func (f *ConditionOptions) getACopy() ConditionOptions {
 	return ConditionOptions{
 		Names:               cloneConditionPatterns(f.Names),
 		GroupNames:          cloneConditionPatterns(f.GroupNames),
+		RoleNames:           cloneConditionPatterns(f.RoleNames),
 		FsPaths:             cloneConditionPatterns(f.FsPaths),
 		Protocols:           protocols,
 		ProviderObjects:     providerObjects,
@@ -1050,21 +1053,19 @@ func (f *ConditionOptions) getACopy() ConditionOptions {
 }
 
 func (f *ConditionOptions) validate() error {
-	for _, name := range f.Names {
-		if err := name.validate(); err != nil {
-			return err
-		}
+	if err := validateConditionPatterns(f.Names); err != nil {
+		return err
 	}
-	for _, name := range f.GroupNames {
-		if err := name.validate(); err != nil {
-			return err
-		}
+	if err := validateConditionPatterns(f.GroupNames); err != nil {
+		return err
 	}
-	for _, fsPath := range f.FsPaths {
-		if err := fsPath.validate(); err != nil {
-			return err
-		}
+	if err := validateConditionPatterns(f.RoleNames); err != nil {
+		return err
 	}
+	if err := validateConditionPatterns(f.FsPaths); err != nil {
+		return err
+	}
+
 	for _, p := range f.Protocols {
 		if !util.Contains(SupportedRuleConditionProtocols, p) {
 			return util.NewValidationError(fmt.Sprintf("unsupported rule condition protocol: %q", p))
@@ -1192,6 +1193,7 @@ func (c *EventConditions) validate(trigger int) error {
 		c.ProviderEvents = nil
 		c.Options.Names = nil
 		c.Options.GroupNames = nil
+		c.Options.RoleNames = nil
 		c.Options.FsPaths = nil
 		c.Options.Protocols = nil
 		c.Options.MinFileSize = 0
@@ -1201,6 +1203,7 @@ func (c *EventConditions) validate(trigger int) error {
 		c.FsEvents = nil
 		c.ProviderEvents = nil
 		c.Options.GroupNames = nil
+		c.Options.RoleNames = nil
 		c.Options.FsPaths = nil
 		c.Options.Protocols = nil
 		c.Options.MinFileSize = 0
@@ -1445,6 +1448,15 @@ func cloneConditionPatterns(patterns []ConditionPattern) []ConditionPattern {
 	return res
 }
 
+func validateConditionPatterns(patterns []ConditionPattern) error {
+	for _, name := range patterns {
+		if err := name.validate(); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 // Task stores the state for a scheduled task
 type Task struct {
 	Name     string `json:"name"`

+ 34 - 2
internal/httpd/httpd_test.go

@@ -1541,6 +1541,14 @@ func TestActionRuleRelations(t *testing.T) {
 					Month:      "*",
 				},
 			},
+			Options: dataprovider.ConditionOptions{
+				RoleNames: []dataprovider.ConditionPattern{
+					{
+						Pattern:      "g*",
+						InverseMatch: true,
+					},
+				},
+			},
 		},
 		Actions: []dataprovider.EventAction{
 			{
@@ -2008,6 +2016,16 @@ func TestEventRuleValidation(t *testing.T) {
 	_, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest)
 	assert.NoError(t, err)
 	assert.Contains(t, string(resp), "unsupported provider event")
+	rule.Conditions.ProviderEvents = []string{"add"}
+	rule.Conditions.Options.RoleNames = []dataprovider.ConditionPattern{
+		{
+			Pattern: "",
+		},
+	}
+	_, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest)
+	assert.NoError(t, err)
+	assert.Contains(t, string(resp), "empty condition pattern not allowed")
+	rule.Conditions.Options.RoleNames = nil
 	rule.Trigger = dataprovider.EventTriggerSchedule
 	_, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest)
 	assert.NoError(t, err)
@@ -2026,8 +2044,8 @@ func TestEventRuleValidation(t *testing.T) {
 			Month:      "*",
 		},
 	}
-	_, _, err = httpdtest.AddEventRule(rule, http.StatusInternalServerError)
-	assert.NoError(t, err)
+	_, resp, err = httpdtest.AddEventRule(rule, http.StatusInternalServerError)
+	assert.NoError(t, err, string(resp))
 }
 
 func TestUserTransferLimits(t *testing.T) {
@@ -20180,6 +20198,12 @@ func TestWebEventRule(t *testing.T) {
 						InverseMatch: true,
 					},
 				},
+				RoleNames: []dataprovider.ConditionPattern{
+					{
+						Pattern:      "g*",
+						InverseMatch: true,
+					},
+				},
 			},
 		},
 		Actions: []dataprovider.EventAction{
@@ -20211,6 +20235,8 @@ func TestWebEventRule(t *testing.T) {
 	form.Set("type_name_pattern0", "inverse")
 	form.Set("group_name_pattern0", rule.Conditions.Options.GroupNames[0].Pattern)
 	form.Set("type_group_name_pattern0", "inverse")
+	form.Set("role_name_pattern0", rule.Conditions.Options.RoleNames[0].Pattern)
+	form.Set("type_role_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")
@@ -20309,6 +20335,12 @@ func TestWebEventRule(t *testing.T) {
 					InverseMatch: true,
 				},
 			},
+			RoleNames: []dataprovider.ConditionPattern{
+				{
+					Pattern:      "g*",
+					InverseMatch: true,
+				},
+			},
 			FsPaths: []dataprovider.ConditionPattern{
 				{
 					Pattern: "/subdir/*.txt",

+ 13 - 1
internal/httpd/webadmin.go

@@ -2169,7 +2169,7 @@ func getEventActionFromPostFields(r *http.Request) (dataprovider.BaseEventAction
 
 func getEventRuleConditionsFromPostFields(r *http.Request) (dataprovider.EventConditions, error) {
 	var schedules []dataprovider.Schedule
-	var names, groupNames, fsPaths []dataprovider.ConditionPattern
+	var names, groupNames, roleNames, fsPaths []dataprovider.ConditionPattern
 	for k := range r.Form {
 		if strings.HasPrefix(k, "schedule_hour") {
 			hour := r.Form.Get(k)
@@ -2208,6 +2208,17 @@ func getEventRuleConditionsFromPostFields(r *http.Request) (dataprovider.EventCo
 				})
 			}
 		}
+		if strings.HasPrefix(k, "role_name_pattern") {
+			pattern := r.Form.Get(k)
+			if pattern != "" {
+				idx := strings.TrimPrefix(k, "role_name_pattern")
+				patternType := r.Form.Get(fmt.Sprintf("type_role_name_pattern%s", idx))
+				roleNames = append(roleNames, dataprovider.ConditionPattern{
+					Pattern:      pattern,
+					InverseMatch: patternType == inversePatternType,
+				})
+			}
+		}
 		if strings.HasPrefix(k, "fs_path_pattern") {
 			pattern := r.Form.Get(k)
 			if pattern != "" {
@@ -2235,6 +2246,7 @@ func getEventRuleConditionsFromPostFields(r *http.Request) (dataprovider.EventCo
 		Options: dataprovider.ConditionOptions{
 			Names:               names,
 			GroupNames:          groupNames,
+			RoleNames:           roleNames,
 			FsPaths:             fsPaths,
 			Protocols:           r.Form["fs_protocols"],
 			ProviderObjects:     r.Form["provider_objects"],

+ 3 - 0
internal/httpdtest/httpdtest.go

@@ -1510,6 +1510,9 @@ func checkEventConditionOptions(expected, actual dataprovider.ConditionOptions)
 	if err := compareConditionPatternOptions(expected.GroupNames, actual.GroupNames); err != nil {
 		return errors.New("condition group names mismatch")
 	}
+	if err := compareConditionPatternOptions(expected.RoleNames, actual.RoleNames); err != nil {
+		return errors.New("condition role names mismatch")
+	}
 	if err := compareConditionPatternOptions(expected.FsPaths, actual.FsPaths); err != nil {
 		return errors.New("condition fs_paths mismatch")
 	}

+ 4 - 0
openapi/openapi.yaml

@@ -6500,6 +6500,10 @@ components:
           type: array
           items:
             $ref: '#/components/schemas/ConditionPattern'
+        role_names:
+          type: array
+          items:
+            $ref: '#/components/schemas/ConditionPattern'
         fs_paths:
           type: array
           items:

+ 84 - 0
templates/webadmin/eventrule.html

@@ -291,6 +291,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 trigger-provider">
+                <div class="card-header">
+                    <b>Role name filters</b>
+                </div>
+                <div class="card-body">
+                    <h6 class="card-title mb-4">Shell-like pattern filters for role names. For example "role*"" will match role names starting with "role".</h6>
+                    <div class="form-group row">
+                        <div class="col-md-12 form_field_role_names_outer">
+                            {{range $idx, $val := .Rule.Conditions.Options.RoleNames}}
+                            <div class="row form_field_role_names_outer_row">
+                                <div class="form-group col-md-8">
+                                    <input type="text" class="form-control" id="idRoleNamePattern{{$idx}}" name="role_name_pattern{{$idx}}" placeholder="" value="{{$val.Pattern}}" maxlength="255">
+                                </div>
+                                <div class="form-group col-md-3">
+                                    <select class="form-control selectpicker" id="idRoleNamePatternType{{$idx}}" name="type_role_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_role_name_pattern_btn_frm_field">
+                                        <i class="fas fa-trash"></i>
+                                    </button>
+                                </div>
+                            </div>
+                            {{else}}
+                            <div class="row form_field_role_names_outer_row">
+                                <div class="form-group col-md-8">
+                                    <input type="text" class="form-control" id="idRoleNamePattern0" name="role_name_pattern0" placeholder="" value="" maxlength="255">
+                                </div>
+                                <div class="form-group col-md-3">
+                                    <select class="form-control selectpicker" id="idRoleNamePatternType0" name="type_role_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_role_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_role_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>
@@ -542,6 +596,36 @@ along with this program.  If not, see <https://www.gnu.org/licenses/>.
         $(this).closest(".form_field_group_names_outer_row").remove();
     });
 
+    $("body").on("click", ".add_new_role_name_pattern_field_btn", function () {
+        var index = $(".form_field_role_names_outer").find(".form_field_role_names_outer_row").length;
+        while (document.getElementById("idRoleNamePattern"+index) != null){
+            index++;
+        }
+        $(".form_field_role_names_outer").append(`
+            <div class="row form_field_role_names_outer_row">
+                <div class="form-group col-md-8">
+                    <input type="text" class="form-control" id="idRoleNamePattern${index}" name="role_name_pattern${index}" placeholder="" value="" maxlength="255">
+                </div>
+                <div class="form-group col-md-3">
+                    <select class="form-control" id="idRoleNamePatternType${index}" name="type_role_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_role_name_pattern_btn_frm_field">
+                        <i class="fas fa-trash"></i>
+                    </button>
+                </div>
+            </div>
+        `);
+        $("#idRoleNamePatternType"+index).selectpicker();
+    });
+
+    $("body").on("click", ".remove_role_name_pattern_btn_frm_field", function () {
+        $(this).closest(".form_field_role_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){