RuleFilter.filter()   F
last analyzed

Complexity

Conditions 9

Size

Total Lines 49

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
c 0
b 0
f 0
dl 0
loc 49
rs 3.75
1
# Licensed to the StackStorm, Inc ('StackStorm') under one or more
2
# contributor license agreements.  See the NOTICE file distributed with
3
# this work for additional information regarding copyright ownership.
4
# The ASF licenses this file to You under the Apache License, Version 2.0
5
# (the "License"); you may not use this file except in compliance with
6
# the License.  You may obtain a copy of the License at
7
#
8
#     http://www.apache.org/licenses/LICENSE-2.0
9
#
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
# See the License for the specific language governing permissions and
14
# limitations under the License.
15
16
import six
17
import json
18
import re
19
20
from st2common import log as logging
21
import st2common.operators as criteria_operators
22
from st2common.constants.rules import RULE_TYPE_BACKSTOP, MATCH_CRITERIA
23
24
from st2common.util.payload import PayloadLookup
25
from st2common.util.templating import render_template_with_system_context
26
27
28
LOG = logging.getLogger('st2reactor.ruleenforcement.filter')
29
30
31
class RuleFilter(object):
32
    def __init__(self, trigger_instance, trigger, rule, extra_info=False):
33
        """
34
        :param trigger_instance: TriggerInstance DB object.
35
        :type trigger_instance: :class:`TriggerInstanceDB``
36
37
        :param trigger: Trigger DB object.
38
        :type trigger: :class:`TriggerDB`
39
40
        :param rule: Rule DB object.
41
        :type rule: :class:`RuleDB`
42
        """
43
        self.trigger_instance = trigger_instance
44
        self.trigger = trigger
45
        self.rule = rule
46
        self.extra_info = extra_info
47
48
        # Base context used with a logger
49
        self._base_logger_context = {
50
            'rule': self.rule,
51
            'trigger': self.trigger,
52
            'trigger_instance': self.trigger_instance
53
        }
54
55
    def filter(self):
56
        """
57
        Return true if the rule is applicable to the provided trigger instance.
58
59
        :rtype: ``bool``
60
        """
61
        LOG.info('Validating rule %s for %s.', self.rule.ref, self.trigger['name'],
62
                 extra=self._base_logger_context)
63
64
        if not self.rule.enabled:
65
            if self.extra_info:
66
                LOG.info('Validation failed for rule %s as it is disabled.', self.rule.ref)
67
            return False
68
69
        criteria = self.rule.criteria
70
        is_rule_applicable = True
71
72
        if criteria and not self.trigger_instance.payload:
73
            return False
74
75
        payload_lookup = PayloadLookup(self.trigger_instance.payload)
76
77
        LOG.debug('Trigger payload: %s', self.trigger_instance.payload,
78
                  extra=self._base_logger_context)
79
80
        for (criterion_k, criterion_v) in six.iteritems(criteria):
81
            is_rule_applicable, payload_value, criterion_pattern = self._check_criterion(
82
                criterion_k,
83
                criterion_v,
84
                payload_lookup
85
            )
86
            if not is_rule_applicable:
87
                if self.extra_info:
88
                    criteria_extra_info = '\n'.join([
89
                        '  key: %s' % criterion_k,
90
                        '  pattern: %s' % criterion_pattern,
91
                        '  type: %s' % criterion_v['type'],
92
                        '  payload: %s' % payload_value
93
                    ])
94
                    LOG.info('Validation for rule %s failed on criteria -\n%s', self.rule.ref,
95
                             criteria_extra_info,
96
                             extra=self._base_logger_context)
97
                break
98
99
        if not is_rule_applicable:
100
            LOG.debug('Rule %s not applicable for %s.', self.rule.id, self.trigger['name'],
101
                      extra=self._base_logger_context)
102
103
        return is_rule_applicable
104
105
    def _check_criterion(self, criterion_k, criterion_v, payload_lookup):
106
        if 'type' not in criterion_v:
107
            # Comparison operator type not specified, can't perform a comparison
108
            return (False, None, None)
109
110
        criteria_operator = criterion_v['type']
111
        criteria_condition = criterion_v.get('condition', None)
112
        criteria_pattern = criterion_v.get('pattern', None)
113
114
        # Render the pattern (it can contain a jinja expressions)
115
        try:
116
            criteria_pattern = self._render_criteria_pattern(
117
                criteria_pattern=criteria_pattern,
118
                criteria_context=payload_lookup.context
119
            )
120
        except Exception:
121
            LOG.exception('Failed to render pattern value "%s" for key "%s"' %
122
                          (criteria_pattern, criterion_k), extra=self._base_logger_context)
123
            return (False, None, None)
124
125
        try:
126
            matches = payload_lookup.get_value(criterion_k)
127
            # pick value if only 1 matches else will end up being an array match.
128
            if matches:
129
                payload_value = matches[0] if len(matches) > 0 else matches
130
            else:
131
                payload_value = None
132
        except:
133
            LOG.exception('Failed transforming criteria key %s', criterion_k,
134
                          extra=self._base_logger_context)
135
            return (False, None, None)
136
137
        op_func = criteria_operators.get_operator(criteria_operator)
138
139
        try:
140
            if criteria_operator == criteria_operators.SEARCH:
141
                result = op_func(value=payload_value, criteria_pattern=criteria_pattern,
142
                                 criteria_condition=criteria_condition,
143
                                 check_function=self._bool_criterion)
144
            else:
145
                result = op_func(value=payload_value, criteria_pattern=criteria_pattern)
146
        except:
147
            LOG.exception('There might be a problem with the criteria in rule %s.', self.rule,
148
                          extra=self._base_logger_context)
149
            return (False, None, None)
150
151
        return result, payload_value, criteria_pattern
152
153
    def _bool_criterion(self, criterion_k, criterion_v, payload_lookup):
154
        # Pass through to _check_criterion, but pull off and return only the
155
        # final result
156
        return self._check_criterion(criterion_k, criterion_v, payload_lookup)[0]
157
158
    def _render_criteria_pattern(self, criteria_pattern, criteria_context):
159
        # Note: Here we want to use strict comparison to None to make sure that
160
        # other falsy values such as integer 0 are handled correctly.
161
        if criteria_pattern is None:
162
            return None
163
164
        if not isinstance(criteria_pattern, six.string_types):
165
            # We only perform rendering if value is a string - rendering a non-string value
166
            # makes no sense
167
            return criteria_pattern
168
169
        LOG.debug(
170
            'Rendering criteria pattern (%s) with context: %s',
171
            criteria_pattern,
172
            criteria_context
173
        )
174
175
        to_complex = False
176
177
        # Check if jinja variable is in criteria_pattern and if so lets ensure
178
        # the proper type is applied to it using to_complex jinja filter
179
        if len(re.findall(MATCH_CRITERIA, criteria_pattern)) > 0:
180
            LOG.debug("Rendering Complex")
181
            complex_criteria_pattern = re.sub(
182
                MATCH_CRITERIA, r'\1\2 | to_complex\3',
183
                criteria_pattern
184
            )
185
186
            try:
187
                criteria_rendered = render_template_with_system_context(
188
                    value=complex_criteria_pattern,
189
                    context=criteria_context
190
                )
191
                criteria_rendered = json.loads(criteria_rendered)
192
                to_complex = True
193
            except ValueError, error:
194
                LOG.debug('Criteria pattern not valid JSON: %s', error)
195
196
        if not to_complex:
197
            criteria_rendered = render_template_with_system_context(
198
                value=criteria_pattern,
199
                context=criteria_context
200
            )
201
202
        LOG.debug(
203
            'Rendered criteria pattern: %s',
204
            criteria_rendered
205
        )
206
207
        return criteria_rendered
208
209
210
class SecondPassRuleFilter(RuleFilter):
211
    """
212
    Special filter that handles all second pass rules. For not these are only
213
    backstop rules i.e. those that can match when no other rule has matched.
214
    """
215
    def __init__(self, trigger_instance, trigger, rule, first_pass_matched):
216
        """
217
        :param trigger_instance: TriggerInstance DB object.
218
        :type trigger_instance: :class:`TriggerInstanceDB``
219
220
        :param trigger: Trigger DB object.
221
        :type trigger: :class:`TriggerDB`
222
223
        :param rule: Rule DB object.
224
        :type rule: :class:`RuleDB`
225
226
        :param first_pass_matched: Rules that matched in the first pass.
227
        :type first_pass_matched: `list`
228
        """
229
        super(SecondPassRuleFilter, self).__init__(trigger_instance, trigger, rule)
230
        self.first_pass_matched = first_pass_matched
231
232
    def filter(self):
233
        # backstop rules only apply if no rule matched in the first pass.
234
        if self.first_pass_matched and self._is_backstop_rule():
235
            return False
236
        return super(SecondPassRuleFilter, self).filter()
237
238
    def _is_backstop_rule(self):
239
        return self.rule.type['ref'] == RULE_TYPE_BACKSTOP
240